├── tools
├── generators
│ └── .gitkeep
├── tsconfig.tools.json
└── ignore-vercel-build.sh
├── Procfile
├── packages
├── linx-next
│ ├── public
│ │ ├── .gitkeep
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── maskable_icon.png
│ │ ├── apple-touch-icon.png
│ │ ├── mstile-150x150.png
│ │ ├── Lynx - icon large.png
│ │ ├── maskable_icon_x128.png
│ │ ├── maskable_icon_x192.png
│ │ ├── maskable_icon_x512.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── images
│ │ │ └── linkgroupDefaultBackground.png
│ │ ├── fallback-Xdc_5gJKddz2G953z4CJc.js
│ │ ├── browserconfig.xml
│ │ ├── head.html
│ │ ├── Lynx logo.svg
│ │ └── site.webmanifest
│ ├── containers
│ │ ├── SignIn
│ │ │ ├── index.ts
│ │ │ ├── SignIn.styled.ts
│ │ │ └── SignIn.tsx
│ │ ├── SignUp
│ │ │ ├── index.ts
│ │ │ └── SignUp.styled.ts
│ │ ├── MainFeed
│ │ │ ├── index.ts
│ │ │ └── MainFeed.styled.ts
│ │ ├── LandingPage
│ │ │ ├── index.ts
│ │ │ ├── LandingPage.styled.ts
│ │ │ └── LandingPage.tsx
│ │ └── StatsPage
│ │ │ ├── index.ts
│ │ │ ├── StatsPage.tsx
│ │ │ └── StatsPage.styled.ts
│ ├── layouts
│ │ ├── Footer
│ │ │ ├── index.tsx
│ │ │ ├── Footer.styled.ts
│ │ │ └── Footer.tsx
│ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ ├── Header.styled.ts
│ │ │ └── Header.tsx
│ │ ├── UserNav
│ │ │ ├── index.ts
│ │ │ ├── UserNav.styled.ts
│ │ │ └── UserNav.tsx
│ │ ├── AuthLayout
│ │ │ ├── index.tsx
│ │ │ └── AuthLayout.tsx
│ │ └── MainLayout
│ │ │ ├── index.tsx
│ │ │ └── MainLayout.tsx
│ ├── components
│ │ ├── Button
│ │ │ ├── index.ts
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── Button.styled.ts
│ │ ├── TagList
│ │ │ ├── index.ts
│ │ │ ├── TagList.styled.ts
│ │ │ ├── TagList.tsx
│ │ │ └── TagList.test.tsx
│ │ ├── ErrorPanel
│ │ │ ├── index.ts
│ │ │ ├── ErrorPanel.styled.ts
│ │ │ └── ErrorPanel.tsx
│ │ ├── ReviewForm
│ │ │ ├── index.ts
│ │ │ └── ReviewForm.styled.ts
│ │ ├── SearchBar
│ │ │ ├── index.ts
│ │ │ ├── SearchBar.tsx
│ │ │ └── SearchBar.styled.ts
│ │ ├── StatPill
│ │ │ ├── index.ts
│ │ │ ├── StatPill.tsx
│ │ │ └── StatPill.styled.ts
│ │ ├── LinkGroupBody
│ │ │ ├── index.ts
│ │ │ ├── LinkGroupBody.styled.ts
│ │ │ └── LinkGroupBody.tsx
│ │ ├── LinkGroupForm
│ │ │ ├── index.ts
│ │ │ ├── LinkGroupForm.styled.ts
│ │ │ └── LinkGroupForm.tsx
│ │ ├── LogoAppName
│ │ │ ├── index.ts
│ │ │ ├── LogoAppName.styled.ts
│ │ │ └── LogoAppName.tsx
│ │ ├── LynxInfoPanel
│ │ │ ├── index.ts
│ │ │ ├── LynxInfoPanel.tsx
│ │ │ └── LynxInfoPanel.styled.ts
│ │ ├── ReviewStars
│ │ │ ├── index.ts
│ │ │ ├── ReviewStars.styled.ts
│ │ │ ├── ReviewStars.tsx
│ │ │ └── ReviewStars.test.tsx
│ │ ├── SocialButton
│ │ │ ├── index.ts
│ │ │ ├── SocialButton.tsx
│ │ │ └── SocialButton.styled.ts
│ │ ├── UserDropdown
│ │ │ ├── index.ts
│ │ │ ├── UserDropdown.tsx
│ │ │ └── UserDropdown.styled.ts
│ │ ├── AuthLinkFlavor
│ │ │ ├── index.ts
│ │ │ ├── AuthLinkFlavor.tsx
│ │ │ └── AuthLinkFlavor.styled.ts
│ │ ├── CreateLinkGroup
│ │ │ └── index.ts
│ │ ├── ExpandingButton
│ │ │ ├── index.ts
│ │ │ ├── ExpandingButton.test.tsx
│ │ │ └── ExpandingButton.tsx
│ │ ├── LinkComponent
│ │ │ ├── index.ts
│ │ │ ├── LinkComponent.styled.ts
│ │ │ ├── LinkComponent.test.tsx
│ │ │ └── LinkComponent.tsx
│ │ ├── LinkGroupDisplay
│ │ │ ├── index.ts
│ │ │ ├── LinkGroupDisplay.styled.ts
│ │ │ └── LinkGroupDisplay.tsx
│ │ ├── LinkGroupHeader
│ │ │ ├── index.ts
│ │ │ ├── LinkGroupHeader.test.tsx
│ │ │ ├── LinkGroupHeader.styled.ts
│ │ │ └── LinkGroupHeader.tsx
│ │ ├── ReviewComponent
│ │ │ ├── index.ts
│ │ │ ├── ReviewComponent.styled.ts
│ │ │ └── ReviewComponent.tsx
│ │ ├── GithubLoginButton
│ │ │ ├── index.ts
│ │ │ └── GithubLoginButton.tsx
│ │ ├── GoogleLoginButton
│ │ │ ├── index.ts
│ │ │ └── GoogleLoginButton.tsx
│ │ ├── ServiceRouteLinks
│ │ │ ├── index.ts
│ │ │ ├── ServiceRouteLinks.tsx
│ │ │ └── ServiceRouteLinks.styled.ts
│ │ ├── SpecialBackground
│ │ │ ├── index.tsx
│ │ │ └── SpecialBackground.styled.tsx
│ │ └── Text
│ │ │ └── Text.styled.tsx
│ ├── specs
│ │ └── setupTests.ts
│ ├── styles
│ │ ├── breakpoints.scss
│ │ └── global.scss
│ ├── index.d.ts
│ ├── pages
│ │ ├── settings
│ │ │ └── profile.tsx
│ │ ├── api
│ │ │ ├── logout.ts
│ │ │ ├── auth.ts
│ │ │ └── revalidate.ts
│ │ ├── index.tsx
│ │ ├── signup.tsx
│ │ ├── signin.tsx
│ │ ├── 404.tsx
│ │ ├── 500.tsx
│ │ ├── _offline.tsx
│ │ ├── new.tsx
│ │ ├── stats.tsx
│ │ ├── explore.tsx
│ │ ├── t
│ │ │ ├── all.tsx
│ │ │ └── [tag].tsx
│ │ ├── _app.tsx
│ │ ├── u
│ │ │ ├── [user].tsx
│ │ │ └── [user]
│ │ │ │ └── [group].tsx
│ │ └── _document.tsx
│ ├── next-env.d.ts
│ ├── styled.d.ts
│ ├── api
│ │ ├── revalidate.ts
│ │ ├── tag.ts
│ │ ├── review.ts
│ │ ├── link.ts
│ │ ├── linkgroup.ts
│ │ └── user.ts
│ ├── assets
│ │ └── icons
│ │ │ ├── Trash.tsx
│ │ │ ├── OpenInNewTab.tsx
│ │ │ ├── AddIcon.tsx
│ │ │ ├── ChevronDown.tsx
│ │ │ ├── Eye.tsx
│ │ │ ├── Lock.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── GithubOutlineIcon.tsx
│ │ │ ├── index.ts
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── StarEmpty.tsx
│ │ │ ├── StarFull.tsx
│ │ │ ├── LynxLogoDetailNoCircleSmallBox.tsx
│ │ │ ├── LogoSmall.tsx
│ │ │ ├── LynxLogoDetailNoCircle.tsx
│ │ │ ├── LynxLogoDetail.tsx
│ │ │ ├── GithubIcon.tsx
│ │ │ ├── WatchersIcon.tsx
│ │ │ ├── NoConnectionIcon.tsx
│ │ │ └── LinkedAmountIcon.tsx
│ ├── jest.config.ts
│ ├── tsconfig.spec.json
│ ├── hooks
│ │ └── useOutside.tsx
│ ├── .eslintrc.json
│ ├── helpers
│ │ └── fetcher.ts
│ ├── tsconfig.json
│ ├── auth
│ │ └── AuthGate.tsx
│ ├── next.config.js
│ └── project.json
├── linx-next-e2e
│ ├── src
│ │ ├── support
│ │ │ ├── app.po.ts
│ │ │ ├── index.ts
│ │ │ └── commands.ts
│ │ ├── fixtures
│ │ │ └── example.json
│ │ └── integration
│ │ │ └── app.spec.ts
│ ├── tsconfig.json
│ ├── .eslintrc.json
│ ├── cypress.json
│ └── project.json
└── api
│ ├── src
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── assets
│ │ └── favicon.ico
│ ├── app
│ │ ├── lib
│ │ │ ├── db.ts
│ │ │ └── redis.ts
│ │ ├── routes
│ │ │ ├── usergroup
│ │ │ │ └── index.ts
│ │ │ ├── stats
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── google.ts
│ │ │ │ ├── github.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── review
│ │ │ │ └── index.ts
│ │ │ ├── user
│ │ │ │ └── index.ts
│ │ │ ├── tag
│ │ │ │ └── index.ts
│ │ │ ├── link
│ │ │ │ └── index.ts
│ │ │ └── linkgroup
│ │ │ │ └── index.ts
│ │ ├── controllers
│ │ │ ├── stats
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── auth
│ │ │ │ ├── logout.ts
│ │ │ │ ├── me.ts
│ │ │ │ ├── signin.ts
│ │ │ │ ├── signup.ts
│ │ │ │ └── google.ts
│ │ │ ├── tag
│ │ │ │ └── index.ts
│ │ │ ├── user
│ │ │ │ └── index.ts
│ │ │ └── review
│ │ │ │ └── index.ts
│ │ ├── middlewares
│ │ │ ├── measureRequest
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── requireUser.ts
│ │ │ │ └── deserializeUser.ts
│ │ │ ├── cors
│ │ │ │ └── index.ts
│ │ │ ├── cache
│ │ │ │ └── index.ts
│ │ │ └── rateLimit
│ │ │ │ └── index.ts
│ │ ├── helpers
│ │ │ ├── logger.ts
│ │ │ ├── redis.ts
│ │ │ ├── cookie.ts
│ │ │ ├── jwt.ts
│ │ │ ├── utilsJS.ts
│ │ │ ├── pushDiscordWebhook.ts
│ │ │ └── authorizeAndEnd.ts
│ │ └── services
│ │ │ ├── stats.ts
│ │ │ ├── review.ts
│ │ │ ├── user.types.ts
│ │ │ ├── tag.ts
│ │ │ ├── session.ts
│ │ │ └── link.ts
│ ├── interfaces
│ │ └── index.ts
│ └── main.ts
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ ├── tsconfig.app.json
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ └── project.json
├── .prettierrc
├── Banner.png
├── .prettierignore
├── jest.preset.ts
├── jest.preset.js
├── dist
└── packages
│ └── api
│ └── assets
│ └── favicon.ico
├── jest.config.ts
├── workspace.json
├── .vscode
└── extensions.json
├── .env.example
├── .editorconfig
├── tsconfig.base.json
├── .gitignore
├── nx.json
├── .eslintrc.json
├── README.md
└── lynx-logo.svg
/tools/generators/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run start
2 |
--------------------------------------------------------------------------------
/packages/linx-next/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/Banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/Banner.png
--------------------------------------------------------------------------------
/packages/linx-next/containers/SignIn/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './SignIn';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/SignUp/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './SignUp';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Footer';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Header/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Header';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Button';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/TagList/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './TagList';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/MainFeed/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './MainFeed';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/UserNav/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './UserNav';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ErrorPanel/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './ErrorPanel';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewForm/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ReviewForm';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SearchBar/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SearchBar';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/StatPill/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './StatPill';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/LandingPage/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LandingPage';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/StatsPage/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './StatsPage';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/AuthLayout/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './AuthLayout';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/MainLayout/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './MainLayout';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/specs/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/src/support/app.po.ts:
--------------------------------------------------------------------------------
1 | export const getGreeting = () => cy.get('h1');
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupBody/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LinkGroupBody';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupForm/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LinkGroupForm';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LogoAppName/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LogoAppName';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LynxInfoPanel/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LynxInfoPanel';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewStars/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ReviewStars';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SocialButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SocialButton';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/UserDropdown/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './UserDropdown';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/UserNav/UserNav.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/AuthLinkFlavor/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './AuthLinkFlavor';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/CreateLinkGroup/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './CreateLinkGroup';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ExpandingButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ExpandingButton';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkComponent/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './LinkComponent';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupDisplay/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LinkGroupDisplay';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupHeader/index.ts:
--------------------------------------------------------------------------------
1 | export {default} from './LinkGroupHeader';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewComponent/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ReviewComponent';
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 |
--------------------------------------------------------------------------------
/jest.preset.ts:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nrwl/jest/preset');
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/packages/linx-next/components/GithubLoginButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './GithubLoginButton';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/GoogleLoginButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './GoogleLoginButton';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ServiceRouteLinks/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './ServiceRouteLinks';
2 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SpecialBackground/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './SpecialBackground';
2 |
--------------------------------------------------------------------------------
/packages/api/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/packages/api/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false,
3 | };
4 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nrwl/jest/preset').default;
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/dist/packages/api/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/dist/packages/api/assets/favicon.ico
--------------------------------------------------------------------------------
/packages/api/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/api/src/assets/favicon.ico
--------------------------------------------------------------------------------
/packages/linx-next/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/favicon.ico
--------------------------------------------------------------------------------
/packages/linx-next/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/favicon-16x16.png
--------------------------------------------------------------------------------
/packages/linx-next/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/favicon-32x32.png
--------------------------------------------------------------------------------
/packages/linx-next/public/maskable_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/maskable_icon.png
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | const { getJestProjects } = require('@nrwl/jest');
2 |
3 | module.exports = {
4 | projects: getJestProjects(),
5 | };
6 |
--------------------------------------------------------------------------------
/packages/linx-next/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/linx-next/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/mstile-150x150.png
--------------------------------------------------------------------------------
/packages/linx-next-e2e/src/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/linx-next/public/Lynx - icon large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/Lynx - icon large.png
--------------------------------------------------------------------------------
/packages/linx-next/public/maskable_icon_x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/maskable_icon_x128.png
--------------------------------------------------------------------------------
/packages/linx-next/public/maskable_icon_x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/maskable_icon_x192.png
--------------------------------------------------------------------------------
/packages/linx-next/public/maskable_icon_x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/maskable_icon_x512.png
--------------------------------------------------------------------------------
/packages/api/src/app/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const db = new PrismaClient();
4 |
5 | export default db;
6 |
--------------------------------------------------------------------------------
/packages/linx-next/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/packages/linx-next/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/packages/api/src/app/routes/usergroup/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 |
3 | const userGroupRouter = new Router();
4 |
5 | export default userGroupRouter;
6 |
--------------------------------------------------------------------------------
/packages/linx-next/public/images/linkgroupDefaultBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/net-runner/lynx/HEAD/packages/linx-next/public/images/linkgroupDefaultBackground.png
--------------------------------------------------------------------------------
/packages/linx-next/public/fallback-Xdc_5gJKddz2G953z4CJc.js:
--------------------------------------------------------------------------------
1 | (()=>{"use strict";self.fallback=async e=>"document"===e.destination?caches.match("/_offline",{ignoreSearch:!0}):Response.error()})();
--------------------------------------------------------------------------------
/packages/linx-next/styles/breakpoints.scss:
--------------------------------------------------------------------------------
1 | //Predefined Break-points
2 | $mediaMaxWidth: 1260px;
3 | $mediaBp1Width: 960px;
4 | $mediaMinWidth: 480px;
5 |
6 | @mixin breakpoint($size) {}
7 |
--------------------------------------------------------------------------------
/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "projects": {
4 | "api": "packages/api",
5 | "linx-next": "packages/linx-next",
6 | "linx-next-e2e": "packages/linx-next-e2e"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "nrwl.angular-console",
4 | "esbenp.prettier-vscode",
5 | "firsttris.vscode-jest-runner",
6 | "dbaeumer.vscode-eslint"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/linx-next/index.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | declare module '*.svg' {
3 | const content: any;
4 | export const ReactComponent: any;
5 | export default content;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/settings/profile.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | //Page : User settings for profile
4 | const Profile = () => {
5 | return
Profile
;
6 | };
7 | Profile.requireAuth = true;
8 | export default Profile;
9 |
--------------------------------------------------------------------------------
/packages/linx-next/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/stats/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import handleStats from '../../controllers/stats';
3 |
4 | const statRouter = new Router();
5 |
6 | statRouter.get('/', handleStats);
7 |
8 | export default statRouter;
9 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/MainFeed/MainFeed.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | `;
9 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=
2 | API_URL=
3 | GITHUB_APP_SECRET=
4 | GITHUB_APP_ID=
5 | FRONTEND_URL=
6 | DISCORD_WEBHOOK_URL=
7 | GITHUB_HOOK_SECRET=
8 | GOOGLE_APP_ID=
9 | GOOGLE_APP_SECRET=
10 | AUTH_CORE_SECRET=
11 | COOKIE_NAME=
12 | REDIS_TLS_URL=
13 | REDIS_URL=
14 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/linx-next/styled.d.ts:
--------------------------------------------------------------------------------
1 | import 'styled-components';
2 | import { CustomTheme } from './pages/_app';
3 |
4 | declare module 'styled-components' {
5 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
6 | export interface DefaultTheme extends CustomTheme {}
7 | }
8 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/linx-next/api/revalidate.ts:
--------------------------------------------------------------------------------
1 | export async function revalidate(route: string) {
2 | const res = await fetch(`${process.env.FRONTEND_URL}api/revalidate`, {
3 | method: 'POST',
4 | body: JSON.stringify({
5 | refresh_route: route,
6 | }),
7 | });
8 | return res;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "sourceMap": false,
5 | "outDir": "../../dist/out-tsc",
6 | "allowJs": true,
7 | "types": ["cypress", "node"]
8 | },
9 | "include": ["src/**/*.ts", "src/**/*.js"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["node"]
7 | },
8 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"],
9 | "include": ["**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/linx-next/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 00A8AD
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"],
9 | "importHelpers": false
10 | },
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/stats/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultRouteHandler } from '../../../interfaces';
2 | import { getAllStats } from '../../services/stats';
3 |
4 | const handleStats: defaultRouteHandler = async (req, res) => {
5 | const stats = await getAllStats();
6 | return res.status(200).json(stats);
7 | };
8 |
9 | export default handleStats;
10 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SearchBar/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | import { useHotkeys } from 'react-hotkeys-hook';
2 | import * as S from './SearchBar.styled';
3 |
4 | const SearchBar = () => {
5 | //Capute pressing / or CTRL+K for focus
6 | useHotkeys('/, ctrl+k, command+k', () => {
7 | return;
8 | });
9 |
10 | return <>Sb>;
11 | };
12 | export default SearchBar;
13 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LogoAppName/LogoAppName.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const LAN = styled.div`
4 | height: 3rem;
5 | display: flex;
6 | align-items: center;
7 | flex-direction: row;
8 |
9 | & > p {
10 | margin-left: 0.5rem;
11 | font-size: 2.8rem;
12 | font-family: 'Open Sans', sans-serif;
13 | }
14 | `;
15 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/api/logout.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import Cookie from 'cookies';
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | const cookies = new Cookie(req, res);
5 |
6 | cookies.set('access_token');
7 | cookies.set('refresh_token');
8 |
9 | res.status(200).end();
10 | }
11 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | import AuthLayout from '../layouts/AuthLayout/AuthLayout';
3 | import LandingPage from '../containers/LandingPage';
4 |
5 | const Index = () => ;
6 |
7 | Index.getLayout = (page: ReactElement) => {
8 | return {page};
9 | };
10 |
11 | export default Index;
12 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/measureRequest/index.ts:
--------------------------------------------------------------------------------
1 | import log from '../../helpers/logger';
2 |
3 | export const measureRequest = (req, res, next) => {
4 | const start = Date.now();
5 | res.once('finish', () => {
6 | const duration = Date.now() - start;
7 | log.info('Req ' + req.originalUrl + ' processed in: ' + duration + 'ms');
8 | });
9 | next();
10 | };
11 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/logger.ts:
--------------------------------------------------------------------------------
1 | import logger from 'pino';
2 | import * as dayjs from 'dayjs';
3 |
4 | const log = logger({
5 | transport: {
6 | target: 'pino-pretty',
7 | options: {
8 | colorize: true,
9 | },
10 | },
11 | base: {
12 | pid: false,
13 | },
14 | timestamp: () => `,"time":"${dayjs().format()}"`,
15 | });
16 |
17 | export default log;
18 |
--------------------------------------------------------------------------------
/packages/api/src/app/lib/redis.ts:
--------------------------------------------------------------------------------
1 | import log from '../helpers/logger';
2 | import { createClient } from '@redis/client';
3 |
4 | const { REDIS_URL } = process.env;
5 |
6 | const redisClient = createClient({ url: REDIS_URL });
7 |
8 | redisClient.on('ready', () => log.info('[REDIS] Connected'));
9 | redisClient.on('error', (e) => log.error('[REDIS] ' + e));
10 |
11 | export default redisClient;
12 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/api/auth.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
3 | const access_token = req.cookies['access_token'];
4 | const refresh_token = req.cookies['refresh_token'];
5 | res
6 | .status(200)
7 | .json({ hasAuthCookies: access_token || refresh_token ? true : false });
8 | }
9 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewForm/ReviewForm.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StarContainer = styled.div`
4 | background: ${({ theme }) => theme.backgroundTertiary};
5 | height: 2.4rem;
6 | line-height: 2.4rem;
7 | padding: 0 1rem 0 2rem;
8 | border-radius: 1rem;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | `;
13 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ServiceRouteLinks/ServiceRouteLinks.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import * as S from './ServiceRouteLinks.styled';
3 |
4 | const ServiceRouteLinks = () => (
5 |
6 | Offline
7 | 404
8 | 500
9 |
10 | );
11 | export default ServiceRouteLinks;
12 |
--------------------------------------------------------------------------------
/packages/api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/auth/google.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import { GoogleAuthController } from '../../controllers';
3 |
4 | const googleRouter = new Router();
5 | const googleController = new GoogleAuthController();
6 |
7 | googleRouter.get('/', googleController.oauthRedirect);
8 | googleRouter.get('/callback', googleController.oauthCallback);
9 |
10 | export default googleRouter;
11 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/index.ts:
--------------------------------------------------------------------------------
1 | import authRouter from './auth';
2 | import linkRouter from './link';
3 | import linkGroupRouter from './linkgroup';
4 | import tagRouter from './tag';
5 | import userRouter from './user';
6 | import userGroupRouter from './usergroup';
7 |
8 | export {
9 | authRouter,
10 | linkRouter,
11 | linkGroupRouter,
12 | tagRouter,
13 | userRouter,
14 | userGroupRouter,
15 | };
16 |
--------------------------------------------------------------------------------
/packages/linx-next/components/GoogleLoginButton/GoogleLoginButton.tsx:
--------------------------------------------------------------------------------
1 | import { GoogleIcon } from '../../assets/icons';
2 | import React from 'react';
3 | import SocialButton from '../SocialButton';
4 |
5 | const GoogleLoginButton = () => (
6 | }
9 | linkToAuth={'api/auth/signin/google'}
10 | />
11 | );
12 |
13 | export default GoogleLoginButton;
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SearchBar/SearchBar.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: flex-start;
7 | position: fixed;
8 | border-radius: 1rem;
9 | bottom: 3rem;
10 | left: 0.5rem;
11 | font-size: 1.5rem;
12 | & > a {
13 | margin-right: 0.5rem;
14 | opacity: 0.2;
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/index.ts:
--------------------------------------------------------------------------------
1 | import LinkController from './link';
2 | import GithubAuthController from './auth/github';
3 | import GoogleAuthController from './auth/google';
4 | import LinkGroupController from './linkgroup';
5 | import * as UserController from './user';
6 |
7 | export {
8 | LinkController,
9 | GithubAuthController,
10 | GoogleAuthController,
11 | LinkGroupController,
12 | UserController,
13 | };
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewStars/ReviewStars.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div<{ isInput: boolean }>`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | & > svg:not(:first-child) {
8 | margin-left: 0.2rem;
9 | }
10 | ${({ isInput }) => {
11 | return isInput === true ? 'cursor:pointer' : 'cursor:default';
12 | }}
13 | `;
14 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/review/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import { handleReviewAdd, handleReviewDelete } from '../../controllers/review';
3 | import requireUser from '../../middlewares/auth/requireUser';
4 |
5 | const reviewRouter = new Router();
6 |
7 | reviewRouter.post('/add', requireUser, handleReviewAdd);
8 | reviewRouter.delete(':id', requireUser, handleReviewDelete);
9 |
10 | export default reviewRouter;
11 |
--------------------------------------------------------------------------------
/packages/linx-next/components/GithubLoginButton/GithubLoginButton.tsx:
--------------------------------------------------------------------------------
1 | import { GithubOutlineIcon } from '../../assets/icons';
2 | import React from 'react';
3 | import SocialButton from '../SocialButton';
4 |
5 | const GithubLoginButton = () => (
6 | }
9 | linkToAuth={'api/auth/signin/github'}
10 | />
11 | );
12 |
13 | export default GithubLoginButton;
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ServiceRouteLinks/ServiceRouteLinks.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: flex-start;
7 | position: fixed;
8 | border-radius: 1rem;
9 | bottom: 3rem;
10 | left: 0.5rem;
11 | font-size: 1.5rem;
12 | & > a {
13 | margin-right: 0.5rem;
14 | opacity: 0.2;
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/packages/api/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'api',
4 | preset: '../../jest.preset.js',
5 | globals: {
6 | 'ts-jest': {
7 | tsconfig: '/tsconfig.spec.json',
8 | },
9 | },
10 | testEnvironment: 'node',
11 | transform: {
12 | '^.+\\.[tj]s$': 'ts-jest',
13 | },
14 | moduleFileExtensions: ['ts', 'js', 'html'],
15 | coverageDirectory: '../../coverage/packages/api',
16 | };
17 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/Trash.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
11 | );
12 |
13 | export default SvgComponent;
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/Text/Text.styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const SmallAuthText = styled.p`
4 | font-size: 1.2rem;
5 | font-family: 'Poppins', sans-serif;
6 | text-align: center;
7 | font-weight: 400;
8 | `;
9 |
10 | export const AuthImportantText = styled.p`
11 | color: ${({ theme }) => theme.primary};
12 | font-size: 1.6rem;
13 | font-family: 'Poppins', sans-serif;
14 | text-align: center;
15 | `;
16 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["src/plugins/index.js"],
11 | "rules": {
12 | "@typescript-eslint/no-var-requires": "off",
13 | "no-undef": "off"
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/linx-next/components/AuthLinkFlavor/AuthLinkFlavor.tsx:
--------------------------------------------------------------------------------
1 | import * as S from './AuthLinkFlavor.styled';
2 |
3 | interface Props {
4 | type: 'up' | 'down' | 'relative';
5 | }
6 |
7 | const AuthLinkFlavor: React.FC = ({ type }) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | export default AuthLinkFlavor;
16 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/auth/github.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import { GithubAuthController } from '../../controllers';
3 |
4 | const githubRouter = new Router();
5 | const githubController = new GithubAuthController();
6 |
7 | githubRouter.get('/', githubController.oauthRedirect);
8 | githubRouter.get('/callback', githubController.oauthCallback);
9 | githubRouter.post('/hook', githubController.hookEvents);
10 |
11 | export default githubRouter;
12 |
--------------------------------------------------------------------------------
/packages/linx-next/public/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/auth/requireUser.ts:
--------------------------------------------------------------------------------
1 | import { authorizedRouteHandler } from '../../../interfaces';
2 | import log from '../../helpers/logger';
3 |
4 | const requireUser: authorizedRouteHandler = (req, res, next) => {
5 | log.info('[AUTH] USER CHECK REQUESTED FOR ' + req.originalUrl);
6 | const id = res.locals.id;
7 |
8 | if (!id || !id.user || !id.session) return res.status(403).end();
9 |
10 | return next();
11 | };
12 |
13 | export default requireUser;
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/StatPill/StatPill.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './StatPill.styled';
3 |
4 | interface Props {
5 | ico: JSX.Element;
6 | stat: number | string;
7 | isReversed?: boolean;
8 | }
9 |
10 | const StatPill: React.FC = ({ ico, stat, isReversed }) => (
11 |
12 | {stat}
13 | {ico}
14 |
15 | );
16 | export default StatPill;
17 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": ".",
3 | "fixturesFolder": "./src/fixtures",
4 | "integrationFolder": "./src/integration",
5 | "modifyObstructiveCode": false,
6 | "supportFile": "./src/support/index.ts",
7 | "pluginsFile": false,
8 | "video": true,
9 | "videosFolder": "../../dist/cypress/packages/linx-next-e2e/videos",
10 | "screenshotsFolder": "../../dist/cypress/packages/linx-next-e2e/screenshots",
11 | "chromeWebSecurity": false
12 | }
13 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/OpenInNewTab.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
11 | );
12 |
13 | export default SvgComponent;
14 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/signup.tsx:
--------------------------------------------------------------------------------
1 | import { NextSeo } from 'next-seo';
2 | import React, { ReactElement } from 'react';
3 | import AuthLayout from '../layouts/AuthLayout';
4 | import SignUp from "../containers/SignUp";
5 |
6 | const Signup = () =>
7 |
8 | Signup.getLayout = (page: ReactElement) => (
9 |
10 |
11 |
12 | {page}
13 |
14 | );
15 |
16 | export default Signup;
17 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/redis.ts:
--------------------------------------------------------------------------------
1 | import redisClient from '../lib/redis';
2 |
3 | const getFromCache = async (key: string) =>
4 | JSON.parse(await redisClient.get(key));
5 |
6 | const setExCache = async (
7 | key: string,
8 | duration_seconds: number,
9 | value: string
10 | ) => await redisClient.setEx(key, duration_seconds, value);
11 |
12 | const deleteFromCache = async (key: string) => await redisClient.del(key);
13 |
14 | export { getFromCache, setExCache, deleteFromCache };
15 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/user/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import { UserController } from '../../controllers';
3 |
4 | const userRouter = new Router();
5 |
6 | userRouter.get('/all', UserController.handleAllUsers);
7 | userRouter.get('/all/groups', UserController.handleAllUsersWithGroups);
8 | userRouter.get('/:user', UserController.handleAllUsersGroups);
9 | userRouter.get('/:user/g/:group', UserController.handleUserGroupLinks);
10 |
11 | export default userRouter;
12 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/src/integration/app.spec.ts:
--------------------------------------------------------------------------------
1 | import { getGreeting } from '../support/app.po';
2 |
3 | describe('linx-next', () => {
4 | beforeEach(() => cy.visit('/'));
5 |
6 | it('should display welcome message', () => {
7 | // Custom command example, see `../support/commands.ts` file
8 | cy.login('my-email@something.com', 'myPassword');
9 |
10 | // Function helper example, see `../support/app.po.ts` file
11 | getGreeting().contains('Welcome linx-next');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LogoAppName/LogoAppName.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React from 'react';
3 | import LogoSmall from '../../assets/icons/LogoSmall';
4 | import * as S from './LogoAppName.styled';
5 |
6 | const LogoAppName = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | LYNX
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default LogoAppName;
20 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Footer/Footer.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Footer = styled.footer`
4 | z-index: 10;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | height: 3rem;
9 | text-align: center;
10 | font-size: 1.2rem;
11 | font-family: Inter, serif;
12 | background-color: ${({ theme }) => theme.backgroundSecondary};
13 | & > div > a {
14 | font-weight: 500;
15 | text-decoration: underline;
16 | }
17 | `;
18 |
--------------------------------------------------------------------------------
/packages/linx-next/jest.config.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jsdom',
3 | displayName: 'linx-next',
4 | preset: '../../jest.preset.ts',
5 | transform: {
6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
8 | },
9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
10 | coverageDirectory: '../../coverage/packages/linx-next',
11 | setupFilesAfterEnv: ['./specs/setupTests.ts'],
12 | };
13 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/AddIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
14 | );
15 |
16 | export default SvgComponent;
17 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewComponent/ReviewComponent.styled.ts:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import styled from 'styled-components';
3 |
4 | export const ReviewRow = styled.div`
5 | display: flex;
6 | width: 100%;
7 | flex: 1;
8 | flex-grow: 1;
9 | flex-direction: row;
10 | justify-content: space-between;
11 | align-items: space-between;
12 | & a {
13 | font-weight: bold;
14 | }
15 | `;
16 |
17 | export const UserLink = styled(Link)``;
18 | export const DescriptionBlock = styled.p``;
19 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/signin.tsx:
--------------------------------------------------------------------------------
1 | import { NextSeo } from 'next-seo';
2 | import React, { ReactElement } from 'react';
3 | import AuthLayout from '../layouts/AuthLayout';
4 | import SignIn from '../containers/SignIn';
5 |
6 | const SigninPage = () => ;
7 |
8 | SigninPage.getLayout = (page: ReactElement) => {
9 | return (
10 |
11 |
12 | {page}
13 |
14 | );
15 | };
16 |
17 | export default SigninPage;
18 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/stats.ts:
--------------------------------------------------------------------------------
1 | import db from '../lib/db';
2 |
3 | export const getAllStats = async () => {
4 | const allUsers = await db.user.count();
5 | const allLinks = await db.link.count();
6 | const allLinkGroups = await db.linkGroup.count();
7 | const allTags = await db.tag.count();
8 | const allReviews = await db.review.count();
9 |
10 | return {
11 | users: allUsers,
12 | links: allLinks,
13 | linkGroups: allLinkGroups,
14 | tags: allTags,
15 | review: allReviews,
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LynxInfoPanel/LynxInfoPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './LynxInfoPanel.styled';
3 | import AuthLinkFlavor from '../AuthLinkFlavor';
4 |
5 | interface Props {
6 | text: string;
7 | }
8 | const LynxInfoPanel: React.FC = ({ text }) => (
9 |
10 |
11 |
12 | {text}
13 |
14 |
15 | );
16 |
17 | export default LynxInfoPanel;
18 |
--------------------------------------------------------------------------------
/packages/linx-next/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEventHandler } from 'react';
2 | import * as S from './Button.styled';
3 |
4 | interface Props {
5 | children?: JSX.Element | string;
6 | onClick?: MouseEventHandler;
7 | type?: 'button' | 'submit' | 'reset';
8 | isSecondary?: boolean;
9 | }
10 |
11 | const Button: React.FC = ({ children, onClick, type, isSecondary }) => (
12 |
13 | {children}
14 |
15 | );
16 | export default Button;
17 |
--------------------------------------------------------------------------------
/packages/linx-next/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node", "@testing-library/jest-dom"],
7 | "jsx": "react"
8 | },
9 | "include": [
10 | "jest.config.ts",
11 | "**/*.test.ts",
12 | "**/*.spec.ts",
13 | "**/*.test.tsx",
14 | "**/*.spec.tsx",
15 | "**/*.test.js",
16 | "**/*.spec.js",
17 | "**/*.test.jsx",
18 | "**/*.spec.jsx",
19 | "**/*.d.ts"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/linx-next/api/tag.ts:
--------------------------------------------------------------------------------
1 | import { GroupTag, Tag } from '@prisma/client';
2 |
3 | export async function getTags() {
4 | const tags = (await fetch(`${process.env.FRONTEND_URL}api/tag`).then((res) =>
5 | res.json()
6 | )) as (Tag & { _count: { Groups: number } })[];
7 | return tags;
8 | }
9 | export async function addMultipleGroupTags(data: Omit[]) {
10 | const res = await fetch(`${process.env.FRONTEND_URL}api/tag/add/group/many`, {
11 | method: 'POST',
12 | body: JSON.stringify(data),
13 | });
14 | return res;
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "lib": ["es2021", "dom"],
14 | "skipLibCheck": true,
15 | "skipDefaultLibCheck": true,
16 | "baseUrl": ".",
17 | "paths": {}
18 | },
19 | "exclude": ["node_modules", "tmp"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/auth/logout.ts:
--------------------------------------------------------------------------------
1 | import log from '../../helpers/logger';
2 | import { authorizedRouteHandler } from '../../../interfaces';
3 | import { removeSession } from '../../services/session';
4 |
5 | const handleLogout: authorizedRouteHandler = async (req, res) => {
6 | log.info('[USER] Logout for sessionID : ' + res.locals.id.session);
7 |
8 | await removeSession(res.locals.id.session);
9 |
10 | res.clearCookie('access_token');
11 | res.clearCookie('refresh_token');
12 |
13 | res.status(200).end();
14 | };
15 | export default handleLogout;
16 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/cookie.ts:
--------------------------------------------------------------------------------
1 | import { CookieOptions } from 'hyper-express';
2 |
3 | const { COOKIE_DOMAIN } = process.env;
4 | const env = process.env.NODE_ENV;
5 |
6 | const isProduction = env === 'production';
7 |
8 | export const cookieOptions: CookieOptions = {
9 | maxAge: 365 * 24 * 60 * 60,
10 | httpOnly: true,
11 | domain: isProduction ? COOKIE_DOMAIN : 'localhost',
12 | path: '/',
13 | sameSite: 'lax',
14 | secure: isProduction,
15 | };
16 | export const refreshCookieOptions: CookieOptions = {
17 | ...cookieOptions,
18 | maxAge: 365 * 24 * 60 * 60,
19 | };
20 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/ChevronDown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
21 | );
22 |
23 | export default SvgComponent;
24 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupDisplay/LinkGroupDisplay.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | position: relative;
5 | display: flex;
6 | align-items: flex-start;
7 | justify-content: space-between;
8 | flex-direction: column;
9 | width: 80rem;
10 | min-height: 22rem;
11 | margin: 1rem 0 3rem;
12 | border-radius: 2rem;
13 | background: ${({ theme }) => theme.backgroundSecondary};
14 | border: 0.3rem solid ${({ theme }) => theme.backgroundSecondary};
15 | & > * {
16 | z-index: 3;
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SocialButton/SocialButton.tsx:
--------------------------------------------------------------------------------
1 | import router from 'next/router';
2 | import * as S from './SocialButton.styled';
3 |
4 | interface Props {
5 | icon: JSX.Element;
6 | text: string;
7 | linkToAuth: string;
8 | }
9 |
10 | const SocialButton: React.FC = ({ linkToAuth, text, icon }) => {
11 | const handleClick = (href: string) => {
12 | router.push(href);
13 | };
14 | return (
15 | handleClick(linkToAuth)}>
16 | {icon}
17 | {text}
18 |
19 | );
20 | };
21 |
22 | export default SocialButton;
23 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import AuthLayout from '../layouts/AuthLayout';
2 | import { ReactElement } from 'react';
3 | import { NextSeo } from 'next-seo';
4 | import ErrorPanel from '../components/ErrorPanel';
5 |
6 | const Custom404 = () => ;
7 |
8 | Custom404.getLayout = (page: ReactElement) => {
9 | return (
10 |
11 |
15 | {page}
16 |
17 | );
18 | };
19 |
20 | export default Custom404;
21 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/500.tsx:
--------------------------------------------------------------------------------
1 | import AuthLayout from '../layouts/AuthLayout';
2 | import { ReactElement } from 'react';
3 | import { NextSeo } from 'next-seo';
4 | import ErrorPanel from '../components/ErrorPanel';
5 |
6 | const Custom500 = () => ;
7 |
8 | Custom500.getLayout = (page: ReactElement) => {
9 | return (
10 |
11 |
15 | {page}
16 |
17 | );
18 | };
19 |
20 | export default Custom500;
21 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/auth/me.ts:
--------------------------------------------------------------------------------
1 | import { authorizedRouteHandler } from '../../../interfaces';
2 | import log from '../../helpers/logger';
3 | import { hideSelectedObjectKeys } from '../../helpers/utilsJS';
4 | import { getUserById } from '../../services/user';
5 |
6 | const handleMe: authorizedRouteHandler = async (req, res) => {
7 | const usrId = res.locals.id.user;
8 |
9 | log.info('[USER] Get profile: ' + usrId);
10 | const user = await getUserById(res.locals.id.user);
11 | const wuser = hideSelectedObjectKeys(user, ['id', 'password']);
12 |
13 | res.json(wuser);
14 | };
15 |
16 | export default handleMe;
17 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ExpandingButton/ExpandingButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import ExpandingButton from './ExpandingButton';
4 |
5 | describe('ExpandingButton', () => {
6 | const onClickHandler = jest.fn();
7 |
8 | it('renders the button text', () => {
9 | const { getByText } = render(
10 |
17 | );
18 | expect(getByText('Click me')).toBeInTheDocument();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/Eye.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
16 | );
17 |
18 | export default SvgComponent;
19 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/Lock.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
16 | );
17 |
18 | export default SvgComponent;
19 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/MainLayout/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Header from '../Header';
4 |
5 | const Content = styled.div`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | min-height: calc(100vh - 11rem);
10 | `;
11 |
12 | const Wrapper = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 | min-height: 100vh;
16 | overflow: hidden;
17 | `;
18 |
19 | const MainLayout = ({ children }: { children }) => (
20 |
21 |
22 | {children}
23 |
24 | );
25 |
26 | export default MainLayout;
27 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import * as S from './Footer.styled';
3 |
4 | const Footer = () => {
5 | const [isShown, setIsShown] = useState(false);
6 | const emote = isShown ? '🦊' : '❤';
7 | return (
8 | setIsShown(true)}
10 | onMouseLeave={() => setIsShown(false)}
11 | >
12 |
17 |
18 | );
19 | };
20 |
21 | export default Footer;
22 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SocialButton/SocialButton.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Button = styled.button`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | background: transparent;
8 | border-color: rgba(249, 249, 249, 0.25);
9 | border-width: 0.1rem;
10 | height: 4rem;
11 | width: 30rem;
12 | padding: 0.5rem 3rem;
13 | border-radius: 1.5rem;
14 | color: #f9f9f9;
15 | font-weight: bold;
16 | font-size: 1.4rem;
17 | letter-spacing: 0.1rem;
18 | font-family: 'Poppins', sans-serif;
19 | white-space: nowrap;
20 | margin-top: 4rem;
21 | & > svg {
22 | margin-right: 2rem;
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/packages/linx-next/components/AuthLinkFlavor/AuthLinkFlavor.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LinkIcon } from '../../assets/icons';
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | align-items: center;
7 | justify-content: flex-start;
8 | position: absolute;
9 | border-radius: 1rem;
10 | &.up {
11 | left: 3rem;
12 | top: 0.4rem;
13 | }
14 | &.down {
15 | right: 1rem;
16 | bottom: 0;
17 | }
18 | &.relative {
19 | position: relative;
20 | }
21 | `;
22 | export const SpecialLinkIcon = styled(LinkIcon)`
23 | width: 5.5rem;
24 | height: 5.5rem;
25 | transform: rotate(-45deg);
26 | margin-right: 1rem;
27 | `;
28 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/src/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
--------------------------------------------------------------------------------
/packages/linx-next/hooks/useOutside.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | function useOutside(ref, callback) {
4 | useEffect(() => {
5 | /**
6 | * Alert if clicked on outside of element
7 | */
8 | function handleClickOutside(event) {
9 | if (ref.current && !ref.current.contains(event.target)) {
10 | callback();
11 | }
12 | }
13 | // Bind the event listener
14 | document.addEventListener('mousedown', handleClickOutside);
15 | return () => {
16 | // Unbind the event listener on clean up
17 | document.removeEventListener('mousedown', handleClickOutside);
18 | };
19 | }, [ref, callback]);
20 | }
21 | export default useOutside;
22 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewComponent/ReviewComponent.tsx:
--------------------------------------------------------------------------------
1 | import { Review } from '@prisma/client';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import ReviewStars from '../ReviewStars';
5 | import * as S from './ReviewComponent.styled';
6 | interface Props {
7 | data: Review;
8 | }
9 |
10 | const ReviewComponent = ({ data }: Props) => {
11 | return (
12 |
13 |
14 | {'@' + data.creatorName}
15 |
16 |
17 | {data.description}
18 |
19 |
20 | );
21 | };
22 | export default ReviewComponent;
23 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/tag/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import {
3 | handleCreateMultipleGroupTags,
4 | handleCreateTag,
5 | handleGetTagLinkGroups,
6 | handleGetTags,
7 | } from '../../controllers/tag';
8 |
9 | import requireUser from '../../middlewares/auth/requireUser';
10 |
11 | const tagRouter = new Router();
12 |
13 | tagRouter.get('/', handleGetTags);
14 | tagRouter.get('/:tag/g', handleGetTagLinkGroups);
15 |
16 | //Protected create new tag
17 | tagRouter.post('/add', requireUser, handleCreateTag);
18 |
19 | //Protected add multiple group tags
20 | tagRouter.post('/add/group/many', requireUser, handleCreateMultipleGroupTags);
21 |
22 | export default tagRouter;
23 |
--------------------------------------------------------------------------------
/packages/linx-next/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:@nrwl/nx/react-typescript",
4 | "../../.eslintrc.json",
5 | "next",
6 | "next/core-web-vitals"
7 | ],
8 | "ignorePatterns": ["!**/*"],
9 | "overrides": [
10 | {
11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
12 | "rules": {
13 | "@next/next/no-html-link-for-pages": [
14 | "error",
15 | "packages/linx-next/pages"
16 | ]
17 | }
18 | },
19 | {
20 | "files": ["*.ts", "*.tsx"],
21 | "rules": {}
22 | },
23 | {
24 | "files": ["*.js", "*.jsx"],
25 | "rules": {}
26 | }
27 | ],
28 | "env": {
29 | "jest": true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /tmp
5 | /out-tsc
6 | /dist/packages
7 |
8 | # dependencies
9 | node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 | .env
41 | .env*.*
42 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/cors/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultRouteMiddlewareInterface } from '../../../interfaces/index';
2 |
3 | const { FRONTEND_URL } = process.env;
4 | const env = process.env.NODE_ENV;
5 | const isProduction = env === 'production';
6 |
7 | const corsMiddleware: defaultRouteMiddlewareInterface = (req, res, next) => {
8 | res.header(
9 | 'Access-Control-Allow-Origin',
10 | `${isProduction ? FRONTEND_URL : 'http://localhost:4200'}`
11 | );
12 | res.header('Access-Control-Allow-Credentials', 'true');
13 | res.header(
14 | 'Access-Control-Allow-Headers',
15 | 'Origin, X-Requested-With, Content-Type, Accept'
16 | );
17 | next();
18 | };
19 | export default corsMiddleware;
20 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/_offline.tsx:
--------------------------------------------------------------------------------
1 | //Static page for handling app being offline or as a fallback for non WebWorker cached routes
2 |
3 | import { NextSeo } from 'next-seo';
4 | import { ReactElement } from 'react';
5 | import AuthLayout from '../layouts/AuthLayout/AuthLayout';
6 | import ErrorPanel from '../components/ErrorPanel';
7 |
8 | const Offline = () => ;
9 |
10 | Offline.getLayout = (page: ReactElement) => {
11 | return (
12 |
13 |
17 | {page}
18 |
19 | );
20 | };
21 |
22 | export default Offline;
23 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/StatsPage/StatsPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './StatsPage.styled';
3 |
4 | interface StatsInterface {
5 | users: number;
6 | links: number;
7 | linkGroups: number;
8 | tags: number;
9 | review: number;
10 | }
11 |
12 | const StatsPage = ({ stats }: { stats: StatsInterface }) => {
13 | return (
14 |
15 |
16 | Users: {stats.users}
17 | Links: {stats.links}
18 | Tags: {stats.tags}
19 | Reviews: {stats.review}
20 | Groups: {stats.linkGroups}
21 |
22 | );
23 | };
24 |
25 | export default StatsPage;
26 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/UserNav/UserNav.tsx:
--------------------------------------------------------------------------------
1 | import UserDropdown from '../../components/UserDropdown';
2 | import React from 'react';
3 | import ExpandingButton from '../../components/ExpandingButton';
4 | import { useRouter } from 'next/router';
5 |
6 | const UserNav = () => {
7 | const router = useRouter();
8 | const handleClick = (e) => {
9 | router.push(process.env.FRONTEND_URL + 'new');
10 | };
11 | return (
12 | <>
13 |
20 |
21 | >
22 | );
23 | };
24 |
25 | export default UserNav;
26 |
--------------------------------------------------------------------------------
/packages/linx-next/api/review.ts:
--------------------------------------------------------------------------------
1 | import { Review } from '@prisma/client';
2 |
3 | export const addReview = async (data: Omit) => {
4 | try {
5 | return await (
6 | await fetch(`${process.env.FRONTEND_URL}/api/review/add`, {
7 | method: 'POST',
8 | body: JSON.stringify(data),
9 | })
10 | ).json();
11 | } catch (error) {
12 | console.log('E ' + error);
13 | }
14 | };
15 | export const removeReview = async (id: string) => {
16 | try {
17 | return await (
18 | await fetch(`${process.env.FRONTEND_URL}/api/review/${id}`, {
19 | method: 'DELETE',
20 | })
21 | ).json();
22 | } catch (error) {
23 | console.log('E ' + error);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/StatsPage/StatsPage.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LynxLogoDetail } from '../../assets/icons';
3 |
4 | export const Wrapper = styled.div`
5 | z-index: 2;
6 | text-align: start;
7 | position: relative;
8 | padding: 5rem;
9 | border-radius: 2rem;
10 | background: ${({ theme }) => theme.backgroundSecondary};
11 | `;
12 |
13 | export const Info = styled.p`
14 | font-size: 2.5rem;
15 | font-family: Inter, serif;
16 | font-weight: normal;
17 | line-height: 160%;
18 | text-align: start;
19 | margin: 0 1rem;
20 | `;
21 |
22 | export const Logo = styled(LynxLogoDetail)`
23 | width: 15rem;
24 | height: 15rem;
25 | position: absolute;
26 | top: -10rem;
27 | `;
28 |
--------------------------------------------------------------------------------
/packages/linx-next/components/TagList/TagList.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | display: flex;
5 | flex-direction: row;
6 | flex-wrap: wrap;
7 | align-self: flex-start;
8 | `;
9 |
10 | export const TagContainer = styled.div<{ selected: boolean }>`
11 | background-color: ${({ theme, selected }) =>
12 | selected ? theme.primary : theme.backgroundSecondary};
13 | margin: 0.5rem;
14 | border-radius: 0.3rem;
15 | padding: 0.5rem;
16 |
17 | &:hover {
18 | background-color: ${({ theme }) => theme.primary};
19 | }
20 |
21 | & div {
22 | display: inline-block;
23 | cursor: pointer;
24 | padding: 0.5rem;
25 | width: 100%;
26 | height: 100%;
27 | }
28 | `;
29 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/cache/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultRouteMiddlewareInterface } from '../../../interfaces';
2 | import log from '../../helpers/logger';
3 | import redisClient from '../../lib/redis';
4 |
5 | const cache: defaultRouteMiddlewareInterface = async (req, res) => {
6 | const key = req.originalUrl;
7 |
8 | if (req.method !== 'GET') {
9 | log.error('Cannot cache non-GET methods!');
10 | return;
11 | }
12 |
13 | const cachedResponse = await redisClient.get(key);
14 |
15 | if (cachedResponse) {
16 | log.info(`Cache hit for ${key}`);
17 | const response = JSON.parse(cachedResponse);
18 | res.json(response);
19 | } else {
20 | log.info(`Cache miss for ${key}`);
21 | return;
22 | }
23 | };
24 | export default cache;
25 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/new.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import { getTags } from '../api/tag';
3 | import CreateLinkGroup from '../components/CreateLinkGroup';
4 | import MainLayout from '../layouts/MainLayout';
5 |
6 | //New linkgroup creation screen
7 | const New = ({ tags }) => {
8 | return ;
9 | };
10 | export async function getStaticProps(context) {
11 | const tags = await getTags();
12 |
13 | if (tags === null) {
14 | return {
15 | props: { tags: null },
16 | };
17 | } else {
18 | return {
19 | props: { tags },
20 | };
21 | }
22 | }
23 | New.getLayout = (page: ReactElement) => {
24 | return {page};
25 | };
26 | New.requireAuth = true;
27 | export default New;
28 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/stats.tsx:
--------------------------------------------------------------------------------
1 | import { NextSeo } from 'next-seo';
2 | import React, { ReactElement } from 'react';
3 | import AuthLayout from '../layouts/AuthLayout';
4 | import StatsPage from '../containers/StatsPage';
5 |
6 | const Stats = ({ stats }) => ;
7 | export async function getServerSideProps() {
8 | const stats = await fetch(`${process.env.FRONTEND_URL}api/stats/`).then(
9 | (res) => res.json()
10 | );
11 | console.log(stats);
12 | return {
13 | props: { stats },
14 | };
15 | }
16 | Stats.getLayout = (page: ReactElement) => {
17 | return (
18 |
19 |
20 | {page}
21 |
22 | );
23 | };
24 | export default Stats;
25 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/jwt.ts:
--------------------------------------------------------------------------------
1 | import { JwtPayload, sign, SignOptions, verify } from 'jsonwebtoken';
2 | const { AUTH_CORE_SECRET } = process.env;
3 |
4 | declare module 'jsonwebtoken' {
5 | export interface JwtPayload {
6 | user: string;
7 | session: string;
8 | }
9 | }
10 |
11 | export function signJwt(object, options?: SignOptions) {
12 | return sign(object, AUTH_CORE_SECRET, options);
13 | }
14 |
15 | export function verifyJwt(token: string) {
16 | try {
17 | const decoded = verify(token, AUTH_CORE_SECRET) as JwtPayload;
18 | return {
19 | valid: true,
20 | expired: false,
21 | decoded,
22 | };
23 | } catch (e) {
24 | return {
25 | valid: false,
26 | expired: true,
27 | decoded: null,
28 | };
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/rateLimit/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultRouteMiddlewareInterface } from '../../../interfaces/index';
2 | import { RateLimiterMemory } from 'rate-limiter-flexible';
3 | import log from '../../helpers/logger';
4 | const opts = {
5 | points: 24, // 12 points
6 | duration: 1, // Per second
7 | };
8 |
9 | const rateLimiter = new RateLimiterMemory(opts);
10 |
11 | const rateLimiterMiddleware: defaultRouteMiddlewareInterface = (
12 | req,
13 | res,
14 | next
15 | ) => {
16 | rateLimiter
17 | .consume(req.ip)
18 | .then(() => {
19 | next();
20 | })
21 | .catch(() => {
22 | log.error('Too many Requests from: ' + req.ips);
23 | res.status(429).send('Too Many Requests');
24 | });
25 | };
26 | export default rateLimiterMiddleware;
27 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "nx/presets/core.json",
3 | "npmScope": "linx",
4 | "affected": {
5 | "defaultBase": "master"
6 | },
7 | "cli": {
8 | "defaultCollection": "@nrwl/next"
9 | },
10 | "tasksRunnerOptions": {
11 | "default": {
12 | "runner": "@nrwl/nx-cloud",
13 | "options": {
14 | "cacheableOperations": ["build", "lint", "test", "e2e"],
15 | "accessToken": "ZTk3YjdhNjEtNmJkOS00YjViLWIzM2MtOTYwNGU5NDM1OTMwfHJlYWQtd3JpdGU="
16 | }
17 | }
18 | },
19 | "generators": {
20 | "@nrwl/react": {
21 | "application": {
22 | "babel": true
23 | }
24 | },
25 | "@nrwl/next": {
26 | "application": {
27 | "style": "styled-components",
28 | "linter": "eslint"
29 | }
30 | }
31 | },
32 | "defaultProject": "linx-next"
33 | }
34 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourceRoot": "packages/linx-next-e2e/src",
3 | "projectType": "application",
4 | "targets": {
5 | "e2e": {
6 | "executor": "@nrwl/cypress:cypress",
7 | "options": {
8 | "cypressConfig": "packages/linx-next-e2e/cypress.json",
9 | "devServerTarget": "linx-next:serve:development"
10 | },
11 | "configurations": {
12 | "production": {
13 | "devServerTarget": "linx-next:serve:production"
14 | }
15 | }
16 | },
17 | "lint": {
18 | "executor": "@nrwl/linter:eslint",
19 | "outputs": ["{options.outputFile}"],
20 | "options": {
21 | "lintFilePatterns": ["packages/linx-next-e2e/**/*.{js,ts}"]
22 | }
23 | }
24 | },
25 | "tags": [],
26 | "implicitDependencies": ["linx-next"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/linx-next/helpers/fetcher.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { IncomingMessage, ServerResponse } from 'http';
3 | export type FResponse = [error: string | null, data: T | null];
4 |
5 | export const fetcher = async (url: string): Promise> => {
6 | try {
7 | const data: T = await axios.get(url, { withCredentials: true });
8 | return [null, data];
9 | } catch (error) {
10 | return [error, null];
11 | }
12 | };
13 |
14 | export const fetcherSSR = async (
15 | req: IncomingMessage,
16 | res: ServerResponse,
17 | url: string
18 | ): Promise> => {
19 | try {
20 | const data: T = await axios.get(url, {
21 | headers: { cookie: req.headers.cookie },
22 | });
23 | return [null, data];
24 | } catch (error) {
25 | return [error, null];
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/review.ts:
--------------------------------------------------------------------------------
1 | import { Review } from '@prisma/client';
2 | import { deleteFromCache, getFromCache, setExCache } from '../helpers/redis';
3 | import db from '../lib/db';
4 |
5 | export async function createReview(review: Omit) {
6 | const r = await db.review.create({ data: review });
7 | setExCache(r.id, 36000, JSON.stringify(r));
8 | return r;
9 | }
10 | export async function deleteReview(id: string, creatorName: string) {
11 | const cachedReview = await getFromCache(id);
12 |
13 | let rev;
14 | if (cachedReview) {
15 | rev = cachedReview;
16 | } else {
17 | rev = await db.review.findUnique({ where: { id } });
18 | }
19 |
20 | if (rev && rev.creatorName === creatorName) {
21 | deleteFromCache(id);
22 | return await db.review.delete({ where: { id } });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LynxInfoPanel/LynxInfoPanel.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LynxLogoDetail } from '../../assets/icons';
3 |
4 | export const Wrapper = styled.div`
5 | position: relative;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | flex-direction: column;
10 | min-width: 38rem;
11 | margin: 5rem auto;
12 | padding: 0 7rem;
13 | border-radius: 3.3rem;
14 | background-color: ${({ theme }) => theme.background};
15 | `;
16 |
17 | export const Title = styled.h1`
18 | margin: 1rem 0 2rem;
19 | padding: 0 2rem;
20 | font-family: 'Segoe UI', serif;
21 | font-size: 2.8rem;
22 | text-align: center;
23 | font-weight: bold;
24 | `;
25 |
26 | export const Logo = styled(LynxLogoDetail)`
27 | width: 10rem;
28 | height: 10rem;
29 | margin: 2rem 0 1rem;
30 | `;
31 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/user.types.ts:
--------------------------------------------------------------------------------
1 | export interface BasicUser {
2 | email: string;
3 | name: string;
4 | password?: string;
5 | repeat_password?: string;
6 | }
7 | export interface GoogleUser extends BasicUser {
8 | id: string;
9 | verified_email: boolean;
10 | given_name: string;
11 | family_name: string;
12 | picture: string;
13 | locale: string;
14 | }
15 |
16 | export interface GithubUser extends BasicUser {
17 | login: string;
18 | id: number;
19 | node_id: string;
20 | avatar_url: string;
21 | gravatar_id: string;
22 | name: string;
23 | company: string;
24 | blog: string;
25 | bio: string;
26 | followers: number;
27 | following: number;
28 | location: string;
29 | hireable: boolean;
30 | two_factor_authentication: boolean;
31 | }
32 |
33 | export type LynxUser = BasicUser | GoogleUser | GithubUser;
34 |
--------------------------------------------------------------------------------
/packages/linx-next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": false,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "incremental": true,
14 | "types": ["jest", "node", "@testing-library/jest-dom"],
15 | "paths": {
16 | "@/components/*": ["components/*"],
17 | "@/icons/*":["assets/icons/*"],
18 | "@/icons":["assets/icons"],
19 | "@/layouts/*":["layouts/*"],
20 | "@/styles/*":["styles/*"]
21 | }
22 | },
23 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"],
24 | "exclude": ["node_modules", "jest.config.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/link/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import { LinkController } from '../../controllers';
3 | import requireUser from '../../middlewares/auth/requireUser';
4 | import cache from '../../middlewares/cache';
5 |
6 | const linkRouter = new Router();
7 | const linkController = new LinkController();
8 | linkRouter.get('/healthcheck', (req, res) => {
9 | res.status(200).end();
10 | });
11 | linkRouter.post('/add', requireUser, linkController.add);
12 | linkRouter.post('/edit', requireUser, linkController.edit);
13 | linkRouter.post('/del', requireUser, linkController.delete);
14 |
15 | //For getting links no auth required
16 | linkRouter.get('/:id', cache, linkController.getSingle);
17 | linkRouter.get('/:limit/:page', linkController.getMany);
18 | linkRouter.get('/:limit/:page/:skip', linkController.getMany);
19 |
20 | export default linkRouter;
21 |
--------------------------------------------------------------------------------
/packages/linx-next/components/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import Button from './Button';
4 |
5 | describe('Button', () => {
6 | it('renders children prop', () => {
7 | const { getByText } = render();
8 | expect(getByText('Click me')).toBeInTheDocument();
9 | });
10 |
11 | it('calls onClick prop when clicked', () => {
12 | const onClick = jest.fn();
13 | const { getByRole } = render();
14 | fireEvent.click(getByRole('button'));
15 | expect(onClick).toHaveBeenCalled();
16 | });
17 |
18 | it('passes type prop to button element', () => {
19 | const { getByRole } = render();
20 | expect(getByRole('button')).toHaveAttribute('type', 'submit');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nrwl/nx"],
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "rules": {
9 | "@nrwl/nx/enforce-module-boundaries": [
10 | "error",
11 | {
12 | "enforceBuildableLibDependency": true,
13 | "allow": [],
14 | "depConstraints": [
15 | {
16 | "sourceTag": "*",
17 | "onlyDependOnLibsWithTags": ["*"]
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "extends": ["plugin:@nrwl/nx/typescript"],
27 | "rules": {}
28 | },
29 | {
30 | "files": ["*.js", "*.jsx"],
31 | "extends": ["plugin:@nrwl/nx/javascript"],
32 | "rules": {}
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/packages/linx-next/components/SpecialBackground/SpecialBackground.styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LinkIcon } from '../../assets/icons';
3 |
4 | export const SpecialBackgroundContainer = styled.div`
5 | position: fixed;
6 | display: flex;
7 | flex-direction: row;
8 | top: 15vh;
9 | width: 100%;
10 | `;
11 |
12 | export const SpecialBackgroundColumn = styled.div`
13 | display: flex;
14 | flex: 1;
15 | height: 100%;
16 | flex-direction: column;
17 | position: relative;
18 | &:nth-child(2) {
19 | align-items: flex-end;
20 | }
21 | & > svg {
22 | position: absolute;
23 | }
24 | `;
25 |
26 | export const SpecialLinkIcon = styled(LinkIcon)`
27 | width: 8rem;
28 | height: 8rem;
29 | transform: rotate(0);
30 | transition: 3s ease-in-out;
31 | &:hover {
32 | transition: 0.3s ease-in-out;
33 | transform: rotate(700deg) !important;
34 | }
35 | `;
36 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupBody/LinkGroupBody.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | width: 100%;
9 | padding: 3rem 2rem;
10 | border-radius: 0 0 2rem 2rem;
11 | background: ${({ theme }) => theme.background};
12 | `;
13 |
14 | export const Description = styled.p`
15 | width: 100%;
16 | font-size: 1.8rem;
17 | font-weight: normal;
18 | margin-bottom: 0.75rem;
19 | line-height: 160%;
20 | overflow: hidden;
21 | text-overflow: ellipsis;
22 | display: -webkit-box;
23 | -ms-box-orient: vertical;
24 | -moz-box-orient: vertical;
25 | -webkit-box-orient: vertical;
26 | line-clamp: 3;
27 | -webkit-line-clamp: 3;
28 | `;
29 |
30 | export const TagListContainer = styled.div`
31 | width: 100%;
32 | margin-bottom: 1rem;
33 | `;
34 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/LandingPage/LandingPage.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | z-index: 2;
5 | margin: 3rem 20vw;
6 | `;
7 |
8 | export const Header = styled.h1`
9 | font-family: 'Segoe UI', serif;
10 | font-weight: bold;
11 | text-align: center;
12 | line-height: 100%;
13 | font-size: 11rem;
14 | margin-bottom: 6rem;
15 | `;
16 |
17 | export const Info = styled.p`
18 | font-size: 2.5rem;
19 | font-family: Inter, serif;
20 | font-weight: normal;
21 | line-height: 160%;
22 | text-align: center;
23 | margin: 0 1rem;
24 | `;
25 |
26 | export const ButtonContainer = styled.div`
27 | display: flex;
28 | flex-direction: row;
29 | justify-content: space-between;
30 | margin: 4rem 22.5rem;
31 |
32 | & > button {
33 | height: 4.5rem;
34 | }
35 |
36 | & > button:nth-child(2n) {
37 | margin-left: 6rem;
38 | }
39 | `;
40 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/api/revalidate.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | export default async function handler(
3 | req: NextApiRequest,
4 | res: NextApiResponse
5 | ) {
6 | const access_token = req.cookies['access_token'];
7 | const refresh_token = req.cookies['refresh_token'];
8 |
9 | if (!access_token || !refresh_token) {
10 | return res.status(401).json({ message: 'Invalid token' });
11 | }
12 |
13 | const body = JSON.parse(req.body);
14 | const { refresh_route } = body;
15 | if (!refresh_route || typeof refresh_route !== 'string')
16 | return res.status(400).json({ message: 'Bad Request: No paths specified' });
17 |
18 | try {
19 | await res.unstable_revalidate(refresh_route);
20 | return res.json({ revalidated: true });
21 | } catch (err) {
22 | // Catch error and serve 500
23 | return res.status(500).send('Error revalidating');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Header/Header.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Header = styled.header`
4 | z-index: 10;
5 | display: flex;
6 | align-items: center;
7 | justify-items: center;
8 | justify-content: space-between;
9 | background-color: ${({ theme }) => theme.background};
10 | padding: 0 5rem;
11 | height: 8rem;
12 | font-family: Inter, serif;
13 | `;
14 |
15 | export const Nav = styled.nav`
16 | display: flex;
17 | text-align: center;
18 | align-items: center;
19 | justify-items: center;
20 | justify-content: space-between;
21 | a {
22 | text-decoration: none;
23 | font-size: 1.8rem;
24 | font-family: Inter, serif;
25 | font-weight: normal;
26 | &:not(:first-child) {
27 | margin-left: 2rem;
28 | }
29 | }
30 | & button {
31 | height: 3rem;
32 | }
33 | & svg {
34 | display: flex;
35 | align-self: center;
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/packages/linx-next/api/link.ts:
--------------------------------------------------------------------------------
1 | import { Link } from '@prisma/client';
2 |
3 | export const addLink = async (
4 | link: string,
5 | description: string,
6 | privacyLevel = 0,
7 | groupId?: string
8 | ): Promise => {
9 | try {
10 | return await (
11 | await fetch(`${process.env.FRONTEND_URL}/api/link/add`, {
12 | method: 'POST',
13 | body: JSON.stringify({
14 | link,
15 | description,
16 | privacyLevel,
17 | groupId,
18 | }),
19 | })
20 | ).json();
21 | } catch (error) {
22 | console.log('E ' + error);
23 | }
24 | };
25 |
26 | export const removeLink = async (id: string) => {
27 | try {
28 | return await fetch(`${process.env.FRONTEND_URL}/api/link/del`, {
29 | method: 'POST',
30 | body: JSON.stringify({
31 | id,
32 | }),
33 | });
34 | } catch (error) {
35 | console.log('E ' + error);
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
19 | );
20 |
21 | export default SvgComponent;
22 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/AuthLayout/AuthLayout.tsx:
--------------------------------------------------------------------------------
1 | import SpecialBackground from '../../components/SpecialBackground';
2 | import React from 'react';
3 | import styled from 'styled-components';
4 | import Footer from '../Footer/Footer';
5 | import Header from '../Header';
6 | import ServiceRouteLinks from '../../components/ServiceRouteLinks';
7 |
8 | const Content = styled.div`
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | min-height: calc(100vh - 11rem);
13 | `;
14 |
15 | const Wrapper = styled.div`
16 | display: flex;
17 | flex-direction: column;
18 | min-height: 100vh;
19 | overflow: hidden;
20 | `;
21 |
22 | const AuthLayout = ({ children }) => {
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default AuthLayout;
35 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ExpandingButton/ExpandingButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './ExpandingButton.styled';
3 |
4 | interface Props {
5 | onClickHandler: (event: React.MouseEvent) => void;
6 | text: string;
7 | type: 'static' | 'dynamic';
8 | size: 'small' | 'big';
9 | site: 'right' | 'left';
10 | }
11 |
12 | const ExpandingButton = ({ onClickHandler, text, type, size, site }: Props) => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {text}
21 |
22 |
23 |
24 |
25 | );
26 |
27 | export default ExpandingButton;
28 |
--------------------------------------------------------------------------------
/tools/ignore-vercel-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Name of the app to check. Change this to your application name!
3 | APP=linx-next
4 |
5 | # Determine version of Nx installed
6 | NX_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@nrwl/workspace'])")
7 | TS_VERSION=$(node -e "console.log(require('./package.json').devDependencies['typescript'])")
8 |
9 | # Install @nrwl/workspace in order to run the affected command
10 | npm install -D @nrwl/workspace@$NX_VERSION --prefer-offline
11 | npm install -D typescript@$TS_VERSION --prefer-offline
12 |
13 | # Run the affected command, comparing latest commit to the one before that
14 | npx nx affected:apps --plain --base HEAD~1 --head HEAD | grep $APP -q
15 |
16 | # Store result of the previous command (grep)
17 |
18 | IS_AFFECTED=$?
19 |
20 | if [ $IS_AFFECTED -eq 1 ]; then
21 | echo "🛑 - Build cancelled"
22 | exit 0
23 | elif [ $IS_AFFECTED -eq 0 ]; then
24 | echo "✅ - Build can proceed"
25 | exit 1
26 | fi
27 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/explore.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 | import MainLayout from '../layouts/MainLayout';
3 | import MainFeed from '../containers/MainFeed';
4 | import { GetServerSideProps } from 'next';
5 | import { getGroups } from '../api/linkgroup';
6 | import { getTags } from '../api/tag';
7 |
8 | const Explore = ({ linkGroupData, tags }) => (
9 |
10 | );
11 |
12 | export const getStaticProps: GetServerSideProps = async () => {
13 | const tags = await getTags();
14 | let linkGroupData;
15 | try {
16 | linkGroupData = await getGroups(7);
17 | } catch (err) {
18 | linkGroupData = { error: { message: err.message } };
19 | }
20 | // Pass data to the page via props
21 | return { props: { linkGroupData, tags } };
22 | };
23 |
24 | Explore.getLayout = (page: ReactElement) => {
25 | return {page};
26 | };
27 |
28 | export default Explore;
29 |
--------------------------------------------------------------------------------
/packages/linx-next/components/StatPill/StatPill.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div<{ isReversed: boolean }>`
4 | cursor: pointer;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | flex-direction: ${({ isReversed }) => isReversed && "row-reverse"};
9 | height: 2.4rem;
10 | line-height: 2.4rem;
11 | padding: 0 1rem 0 2rem;
12 | border-radius: 1rem;
13 | font-weight: bold;
14 | font-size: 1.4rem;
15 | background: ${({ theme }) => theme.backgroundTertiary};
16 | color: ${({ theme }) => theme.white};
17 | & svg {
18 | fill: ${({ theme }) => theme.white};
19 | }
20 | & > div:first-child {
21 | margin-left: ${({ isReversed }) => isReversed && "1.5rem"};
22 | margin-right: ${({ isReversed }) => !isReversed && "1.5rem"};
23 | }
24 |
25 | `;
26 |
27 | export const IconWrapper = styled.div`
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | `;
32 |
--------------------------------------------------------------------------------
/packages/api/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Response,
3 | DefaultResponseLocals,
4 | Request,
5 | DefaultRequestLocals,
6 | MiddlewareNext,
7 | } from 'hyper-express';
8 | import { JwtPayload } from 'jsonwebtoken';
9 |
10 | export enum ControllerMethodTypes {
11 | ADD,
12 | EDIT,
13 | }
14 |
15 | export interface defaultRouteMiddlewareInterface {
16 | (
17 | req: Request,
18 | res: Response,
19 | next?: MiddlewareNext
20 | );
21 | }
22 | export interface defaultRouteHandler {
23 | (req: Request, res: Response);
24 | }
25 |
26 | interface AuthorizedRouteLocals {
27 | id: JwtPayload;
28 | }
29 | export interface authorizedRouteHandler {
30 | (
31 | req: Request,
32 | res: Response,
33 | next?: MiddlewareNext
34 | );
35 | }
36 |
37 | export enum PrivacyLevels {
38 | PUBLIC = 0,
39 | FRIENDS = 3,
40 | PRIVATE = 6,
41 | }
42 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/utilsJS.ts:
--------------------------------------------------------------------------------
1 | export const showSelectedObjectKeys = (
2 | originalObject: object,
3 | keysToShow: string[]
4 | ): object => {
5 | const filteredObject = {};
6 | Object.keys(originalObject).forEach((key) => {
7 | if (keysToShow.includes(key)) filteredObject[key] = originalObject[key];
8 | });
9 | return filteredObject;
10 | };
11 |
12 | export const hideObjectKeysWithoutValues = (
13 | originalObject: object
14 | ): object => {
15 | const filteredObject = {};
16 | Object.keys(originalObject).forEach((key) => {
17 | if (originalObject[key] !== undefined) filteredObject[key] = originalObject[key];
18 | });
19 | return filteredObject;
20 | };
21 |
22 | export const hideSelectedObjectKeys = (
23 | originalObject: object,
24 | keysToHide: string[]
25 | ): object => {
26 | const filteredObject = {};
27 | Object.keys(originalObject).forEach((key) => {
28 | if (!keysToHide.includes(key)) filteredObject[key] = originalObject[key];
29 | });
30 | return filteredObject;
31 | };
32 |
--------------------------------------------------------------------------------
/packages/linx-next/auth/AuthGate.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect } from 'react';
3 | import { useUser } from '../context/user.context';
4 |
5 | const AuthGate = ({ children }) => {
6 | const { user, isLoading, setRedirect } = useUser();
7 | const router = useRouter();
8 |
9 | useEffect(() => {
10 | if (!isLoading) {
11 | //auth is initialized and there is no user
12 | if (!user) {
13 | // remember the page that user tried to access
14 | setRedirect(router.route);
15 | // redirect
16 | router.push('/signin');
17 | }
18 | }
19 | }, [isLoading, router, user, setRedirect]);
20 |
21 | if (isLoading) {
22 | return Application Loading
;
23 | }
24 |
25 | // if auth initialized with a valid user show protected page
26 | if (!isLoading && user) {
27 | return <>{children}>;
28 | }
29 |
30 | /* otherwise don't return anything, will do a redirect from useEffect */
31 | return null;
32 | };
33 |
34 | export default AuthGate;
35 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/GithubOutlineIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const SvgComponent = (props) => (
4 |
26 | );
27 |
28 | export default SvgComponent;
29 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import handleLogout from '../../controllers/auth/logout';
3 | import handleMe from '../../controllers/auth/me';
4 | import handleSignin from '../../controllers/auth/signin';
5 | import handleSignup from '../../controllers/auth/signup';
6 | import requireUser from '../../middlewares/auth/requireUser';
7 | import githubRouter from './github';
8 | import googleRouter from './google';
9 |
10 | const authRouter = new Router();
11 |
12 | authRouter.get('/authcheck', requireUser, async (req, res) => {
13 | res.send('You are authorized');
14 | });
15 | authRouter.get('/healthcheck', (req, res) => {
16 | res.status(200).end();
17 | });
18 | authRouter.get('/me', requireUser, handleMe);
19 |
20 | authRouter.get('/logout', requireUser, handleLogout);
21 |
22 | authRouter.post('/signin', handleSignin);
23 | authRouter.post('/signup', handleSignup);
24 |
25 | authRouter.use('/signin/github', githubRouter);
26 | authRouter.use('/signin/google', googleRouter);
27 |
28 | export default authRouter;
29 |
--------------------------------------------------------------------------------
/packages/linx-next/components/Button/Button.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | interface Props {
4 | isSecondary?: boolean;
5 | }
6 |
7 | export const Button = styled.button`
8 | cursor: pointer;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | padding: 0.5rem 3rem;
13 | border-radius: 1rem;
14 | font-weight: bold;
15 | font-size: 1.4rem;
16 | font-family: 'Poppins', sans-serif;
17 | white-space: nowrap;
18 | background: transparent;
19 | height: 4rem;
20 | width: 30rem;
21 | transition: border-color 0.3s ease, filter 0.3s ease;
22 | ${({ theme, isSecondary }) => {
23 | if (isSecondary)
24 | return `
25 | background: transparent;
26 | border: 0.1rem solid rgba(249, 249, 249, 0.25);
27 | color: #f9f9f9;
28 | `;
29 | return `
30 | background: ${theme.primary};
31 | color: #000;
32 | `;
33 | }};
34 | &:hover {
35 | border-color: rgba(249, 249, 249, 0.5);
36 | filter: saturate(1.1) brightness(1.1);
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupDisplay/LinkGroupDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React, { Ref } from 'react';
2 | import * as S from './LinkGroupDisplay.styled';
3 | import { GroupTag, Link as L, LinkGroup, Review, Tag } from '@prisma/client';
4 | import LinkGroupHeader from '../LinkGroupHeader';
5 | import LinkGroupBody from '../LinkGroupBody';
6 |
7 | interface Props {
8 | data: LinkGroup & {
9 | tags: GroupTag[];
10 | links?: L[];
11 | reviews?: Review[];
12 | };
13 | tags: (Tag & { _count: { Groups: number } })[];
14 | forwardedRef?: Ref;
15 | addNewLinkToState?: (link: L) => void;
16 | }
17 |
18 | const LinkGroupDisplay: React.FC = ({
19 | data,
20 | forwardedRef,
21 | tags,
22 | addNewLinkToState,
23 | }) => (
24 |
25 |
26 |
31 |
32 | );
33 | LinkGroupDisplay.displayName = 'LinkGroupDisplay';
34 |
35 | export default LinkGroupDisplay;
36 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/pushDiscordWebhook.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import log from './logger';
3 | const { DISCORD_WEBHOOK_URL } = process.env;
4 |
5 | //See https://discord.com/developers/docs/resources/channel#embed-object
6 | interface WebhookBody {
7 | title?: string;
8 | type?: string;
9 | description?: string;
10 | url?: string;
11 | timestamp?: string;
12 | color?: string;
13 | }
14 | interface WebhookEmbeds {
15 | embeds: WebhookBody[];
16 | }
17 |
18 | export const pushDiscordWebhook = async (webhook_body: WebhookBody) => {
19 | log.info('[WEBHOOK] New push on discord');
20 | await axios.post(
21 | DISCORD_WEBHOOK_URL,
22 | JSON.stringify({ embeds: [{ ...webhook_body }] }),
23 | {
24 | headers: { 'Content-Type': 'application/json' },
25 | }
26 | );
27 | };
28 |
29 | export const pushDiscordWebhooks = async (webhook_embeds: WebhookEmbeds) => {
30 | log.info('[WEBHOOK] New push on discord');
31 | await axios.post(DISCORD_WEBHOOK_URL, JSON.stringify(webhook_embeds), {
32 | headers: { 'Content-Type': 'application/json' },
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ErrorPanel/ErrorPanel.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | z-index: 2;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | flex-direction: column;
9 | margin: 0 auto;
10 | transform: translateY(-2.5rem);
11 | `;
12 |
13 | export const ErrorNumbers = styled.div`
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | height: 15rem;
18 | font-size: 15rem;
19 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
20 | font-weight: bold;
21 | transform: translateY(-5rem);
22 |
23 | & p:nth-child(2) {
24 | margin: 0 2.5rem;
25 | transform: translateY(2.5rem);
26 | }
27 | `;
28 |
29 | export const LogoContainer = styled.div`
30 | position: relative;
31 | display: flex;
32 | align-items: center;
33 | text-align: center;
34 | margin-bottom: 3rem;
35 | `;
36 |
37 | export const Info = styled.p`
38 | font-size: 2.5rem;
39 | font-family: Inter, serif;
40 | font-weight: normal;
41 | line-height: 160%;
42 | text-align: center;
43 | margin: 0 1rem;
44 | `;
45 |
--------------------------------------------------------------------------------
/packages/linx-next/public/Lynx logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Lynx
6 |
7 | 🦊 Open source platform for sharing, managing and discovering links and bookmarks 🦊
8 |
9 |
10 |
11 |
12 |
13 | ## [Now also on mobile](https://www.github.com/net-runner/lynx-mobile)
14 |
15 | ## Tech Stack
16 | - **Database**: PostgresSQL & Prisma as a ORM
17 | - **Frontend**: Next.js
18 | - **Backend**: Node.js + Hyper-Express
19 | - [**Mobile**](https://www.github.com/net-runner/lynx-mobile): React-Native
20 |
21 | ## Features
22 |
23 | - Dark mode from the start
24 | - Link sharing, discovery, managing, observing link groups or users
25 | - Trending links, users and linkgroups
26 | - Easy import & export
27 | - Clean, modern design
28 | - Fast, and ready as PWA
29 | - Monorepo with all of the code including database structure
30 |
31 | ## [Demo on Vercel](https://lynxweb.vercel.app)
32 |
33 | ## Authors
34 | 🦊 [@net-runner](https://www.github.com/net-runner) & [@przemec](https://www.github.com/przemec) 🦊
35 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkComponent/LinkComponent.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LinkIcon } from '../../assets/icons';
3 |
4 | export const NewTabIconWrapper = styled.a`
5 | cursor: pointer;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | visibility: hidden;
10 | margin-left: 1rem;
11 |
12 | svg {
13 | width: 2.5rem;
14 | height: 2.5rem;
15 | }
16 | `;
17 |
18 | export const DeleteIconWrapper = styled.div`
19 | cursor: pointer;
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | visibility: hidden;
24 | margin-left: 0.8rem;
25 | `;
26 |
27 | export const LContainer = styled.div`
28 | width: 100%;
29 | padding-top: 1rem;
30 | display: flex;
31 | align-items: center;
32 | justify-content: flex-start;
33 |
34 | span {
35 | cursor: pointer;
36 | }
37 |
38 | svg {
39 | width: 2.5rem;
40 | height: 2.5rem;
41 | }
42 |
43 | &:hover > div,
44 | &:hover > ${NewTabIconWrapper} {
45 | visibility: visible;
46 | }
47 | `;
48 |
49 | export const LinkIco = styled(LinkIcon)`
50 | transform: rotate(-45deg);
51 | margin-right: 1rem;
52 | `;
53 |
--------------------------------------------------------------------------------
/packages/api/src/app/routes/linkgroup/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'hyper-express';
2 | import requireUser from '../../middlewares/auth/requireUser';
3 | import { LinkGroupController } from '../../controllers';
4 | import cache from '../../middlewares/cache';
5 |
6 | const linkGroupRouter = new Router();
7 | const linkGroupController = new LinkGroupController();
8 |
9 | linkGroupRouter.post('/add', requireUser, linkGroupController.add);
10 | linkGroupRouter.post('/edit', requireUser, linkGroupController.edit);
11 | linkGroupRouter.post('/del', requireUser, linkGroupController.delete);
12 | linkGroupRouter.post(
13 | '/incrementLinkedCount',
14 | linkGroupController.incrementLinkedCount
15 | );
16 |
17 | linkGroupRouter.get('/:id', cache, linkGroupController.getSingle);
18 | linkGroupRouter.get('/:limit/:page', linkGroupController.getMany);
19 | linkGroupRouter.get('/:limit/:page/:skip', linkGroupController.getMany);
20 | linkGroupRouter.get(
21 | '/:limit/:page/:skip/:privacylevel',
22 | linkGroupController.getMany
23 | );
24 | linkGroupRouter.get(
25 | '/:limit/:page/:skip/:privacylevel/:specificUsername',
26 | linkGroupController.getMany
27 | );
28 |
29 | export default linkGroupRouter;
30 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/t/all.tsx:
--------------------------------------------------------------------------------
1 | import { getTags } from '../../api/tag';
2 | import React, { ReactElement } from 'react';
3 | import { Tag } from '@prisma/client';
4 | import TagList from '../../components/TagList';
5 | import AuthLayout from '../../layouts/AuthLayout';
6 | import styled from 'styled-components';
7 |
8 | export const Wrapper = styled.div`
9 | z-index: 2;
10 | display: flex;
11 | width: 35vw;
12 | align-items: center;
13 | justify-content: center;
14 | flex-direction: column;
15 | margin: 1rem auto 3rem;
16 | `;
17 |
18 | //Page with all tags listed
19 | const AllTags = ({
20 | tags,
21 | }: {
22 | tags: (Tag & { _count: { Groups: number } })[];
23 | }) => {
24 | return (
25 |
26 |
27 |
28 | );
29 | };
30 | export async function getStaticProps() {
31 | const tags = await getTags();
32 |
33 | if (tags === null) {
34 | return {
35 | props: { tags: null },
36 | };
37 | } else {
38 | return {
39 | props: { tags },
40 | };
41 | }
42 | }
43 | AllTags.getLayout = (page: ReactElement) => {
44 | return {page};
45 | };
46 | export default AllTags;
47 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/LandingPage/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './LandingPage.styled';
3 | import { useRouter } from 'next/router';
4 | import Button from '../../components/Button';
5 | import { useUser } from '../../context/user.context';
6 |
7 | const LandingPage: React.FC = () => {
8 | const router = useRouter();
9 | const { user, isAuthenticated } = useUser();
10 | const handleClick = (href: string) => {
11 | router.push(href);
12 | };
13 |
14 | return (
15 |
16 | Manage your links with style
17 |
18 | Lynx helps you manage your bookmarks with ease & enables sharing and
19 | finding new exciting sites with a click of a button.
20 |
21 |
22 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default LandingPage;
36 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/tag/index.ts:
--------------------------------------------------------------------------------
1 | import * as qs from 'qs';
2 | import {
3 | authorizedRouteHandler,
4 | defaultRouteHandler,
5 | } from '../../../interfaces';
6 |
7 | import db from '../../lib/db';
8 | import { getTagLinkGroups, getTags } from '../../services/tag';
9 |
10 | const handleGetTags: defaultRouteHandler = async (req, res) => {
11 | const tags = await getTags();
12 | // log.info(tags);
13 | return res.status(200).json(tags);
14 | };
15 | const handleGetTagLinkGroups: defaultRouteHandler = async (req, res) => {
16 | const tag = Object.keys(qs.parse(req.params.tag))[0];
17 |
18 | const tagLinkGroups = await getTagLinkGroups(tag);
19 |
20 | return res.status(200).json({ tagLinkGroups });
21 | };
22 | const handleCreateTag: authorizedRouteHandler = async (req, res) => {
23 | return res.end();
24 | };
25 | const handleCreateMultipleGroupTags: authorizedRouteHandler = async (
26 | req,
27 | res
28 | ) => {
29 | const body: { groupId: string; tagId: string }[] = await req.json();
30 | await db.groupTag.createMany({ data: body });
31 |
32 | res.status(200).end();
33 | };
34 |
35 | export {
36 | handleGetTagLinkGroups,
37 | handleGetTags,
38 | handleCreateTag,
39 | handleCreateMultipleGroupTags,
40 | };
41 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as GithubIcon } from './GithubIcon';
2 | export { default as GithubOutlineIcon } from './GithubOutlineIcon';
3 | export { default as LinkIcon } from './LinkIcon';
4 | export { default as GoogleIcon } from './GoogleIcon';
5 | export { default as LogoSmall } from './LogoSmall';
6 | export { default as LynxLogoDetail } from './LynxLogoDetail';
7 | export { default as LynxLogoDetailNoCircle } from './LynxLogoDetailNoCircleSmallBox';
8 | export { default as LynxLogoDetailNoCircleSmallBox } from './LynxLogoDetailNoCircleSmallBox';
9 | export { default as NoConnectionIcon } from './NoConnectionIcon';
10 | export { default as ChevronDown } from './ChevronDown';
11 | export { default as LinkedAmountIcon } from './LinkedAmountIcon';
12 | export { default as WatchersIcon } from './WatchersIcon';
13 | export { default as StarEmpty } from './StarEmpty';
14 | export { default as StarHalf } from './StarHalf';
15 | export { default as StarFull } from './StarFull';
16 | export { default as OpenInNewTab } from './OpenInNewTab';
17 | export { default as AddIcon } from './AddIcon';
18 | export { default as Lock } from './Lock';
19 | export { default as Eye } from './Eye';
20 | export { default as Trash } from './Trash';
21 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/user/index.ts:
--------------------------------------------------------------------------------
1 | import { authorizedRouteHandler } from '../../../interfaces';
2 | import {
3 | getAllUsers,
4 | getAllUserGroups,
5 | getAllUserGroupLinks,
6 | getAllUsersWithGroups,
7 | } from '../../services/user';
8 |
9 | const handleAllUsers: authorizedRouteHandler = async (req, res) => {
10 | const users = await getAllUsers();
11 | return res.status(200).json(users);
12 | };
13 | const handleAllUsersWithGroups: authorizedRouteHandler = async (req, res) => {
14 | const users = await getAllUsersWithGroups();
15 | return res.status(200).json(users);
16 | };
17 |
18 | const handleUserGroupLinks: authorizedRouteHandler = async (req, res) => {
19 | const username = req.params.user;
20 | const group = req.params.group;
21 | const links = await getAllUserGroupLinks(username, group);
22 | return res.status(200).json(links);
23 | };
24 |
25 | const handleAllUsersGroups: authorizedRouteHandler = async (req, res) => {
26 | const username = req.params.user;
27 | const userLinkgroups = await getAllUserGroups(username);
28 | return res.status(200).json(userLinkgroups);
29 | };
30 |
31 | export {
32 | handleAllUsersGroups,
33 | handleAllUsers,
34 | handleUserGroupLinks,
35 | handleAllUsersWithGroups,
36 | };
37 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LinkIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
17 | );
18 |
19 | export default SvgComponent;
20 |
--------------------------------------------------------------------------------
/packages/linx-next-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/StarEmpty.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
22 | );
23 |
24 | export default SvgComponent;
25 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/StarFull.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
22 | );
23 |
24 | export default SvgComponent;
25 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/auth/signin.ts:
--------------------------------------------------------------------------------
1 | import * as bcrypt from 'bcrypt';
2 | import { AuthProvider, getUser } from '../../services/user';
3 | import log from '../../helpers/logger';
4 | import { pushDiscordWebhook } from '../../helpers/pushDiscordWebhook';
5 | import { authorizeAndEnd } from '../../helpers/authorizeAndEnd';
6 | import { defaultRouteHandler } from '../../../interfaces';
7 |
8 | //Function for handling user signin using local strategy (email, password)
9 | const handleSignin: defaultRouteHandler = async (req, res) => {
10 | try {
11 | const body = await req.json();
12 | const { email, password } = body;
13 |
14 | const user = await getUser(email);
15 | if (!user) return res.status(404).end();
16 |
17 | const passwordMatch = await bcrypt.compare(password, user.password);
18 | if (!passwordMatch) return res.status(403).end();
19 |
20 | const discordWebhookBody = {
21 | title: `Lynx user logged in: ${body.email}`,
22 | description: `--`,
23 | };
24 | pushDiscordWebhook(discordWebhookBody);
25 |
26 | return authorizeAndEnd(user, req, res, AuthProvider.Local, true);
27 | } catch (e) {
28 | log.error(e);
29 | res
30 | .status(500)
31 | .json({ err: e.message, desc: e.response.data.error_description });
32 | }
33 | };
34 | export default handleSignin;
35 |
--------------------------------------------------------------------------------
/lynx-logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewStars/ReviewStars.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './ReviewStars.styled';
3 | import { StarEmpty, StarFull, StarHalf } from '../../assets/icons';
4 |
5 | interface Props {
6 | rating: number;
7 | isInput?: boolean;
8 | onChange?: (rating: number) => void;
9 | }
10 |
11 | const ReviewStars: React.FC = ({ rating, isInput, onChange }) => {
12 | const handlePress = (index: number) => {
13 | if (isInput) {
14 | if (onChange) {
15 | onChange(index);
16 | }
17 | }
18 | };
19 | const stars = () => {
20 | const stars = [];
21 | for (let i = 0; i < 5; i++) {
22 | if (i + 0.25 < rating && rating < i + 0.75)
23 | stars.push();
24 | else if (i < rating)
25 | stars.push(
26 | handlePress(i + 1)}
29 | key={i}
30 | />
31 | );
32 | else
33 | stars.push(
34 | handlePress(i + 1)}
37 | key={i}
38 | />
39 | );
40 | }
41 | return stars;
42 | };
43 | return {stars()};
44 | };
45 | export default ReviewStars;
46 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/t/[tag].tsx:
--------------------------------------------------------------------------------
1 | import MainLayout from '../../layouts/MainLayout';
2 | import React, { ReactElement } from 'react';
3 | import MainFeed from '../../containers/MainFeed';
4 | import { getTags } from '../../api/tag';
5 |
6 | const TagName = ({ initialLinkGroups, tags }) => (
7 |
14 | );
15 |
16 | export async function getStaticPaths() {
17 | const tags = await getTags();
18 |
19 | const paths = tags.map((t) => ({
20 | params: { tag: t.name },
21 | }));
22 |
23 | return {
24 | paths,
25 | fallback: true,
26 | };
27 | }
28 | export async function getStaticProps(context) {
29 | const { tag } = context.params;
30 | const res = await fetch(`${process.env.FRONTEND_URL}api/tag/${tag}/g`).then(
31 | (res) => res.json()
32 | );
33 | const tags = await getTags();
34 | if (res === null || tags === null) {
35 | return {
36 | props: { initialLinkGroups: null },
37 | };
38 | } else {
39 | const { tagLinkGroups } = res;
40 | return {
41 | props: { initialLinkGroups: tagLinkGroups, tags },
42 | revalidate: 60,
43 | };
44 | }
45 | }
46 |
47 | TagName.getLayout = (page: ReactElement) => {
48 | return {page};
49 | };
50 | export default TagName;
51 |
--------------------------------------------------------------------------------
/packages/linx-next/api/linkgroup.ts:
--------------------------------------------------------------------------------
1 | import { LinkGroup } from '@prisma/client';
2 |
3 | export const getGroups = async (
4 | limit: number,
5 | page = 0,
6 | skip = 0,
7 | privacyLevel = 0,
8 | username?: string
9 | ) => {
10 | try {
11 | return await (
12 | await fetch(
13 | `${
14 | process.env.FRONTEND_URL
15 | }/api/linkgroup/${limit}/${page}/${skip}/${privacyLevel}${
16 | username ? `/${username}` : ''
17 | }`
18 | )
19 | ).json();
20 | } catch (error) {
21 | console.log('E ' + error);
22 | }
23 | };
24 |
25 | export const incrementLinkedCount = async (groupId) => {
26 | try {
27 | await fetch(
28 | `${process.env.FRONTEND_URL}/api/linkgroup/incrementLinkedCount`,
29 | {
30 | method: 'POST',
31 | body: JSON.stringify({
32 | id: groupId,
33 | }),
34 | }
35 | );
36 | } catch (error) {
37 | console.log('E ' + error);
38 | }
39 | };
40 |
41 | export const createGroup = async (data) => {
42 | try {
43 | const newGroup: LinkGroup = await fetch(
44 | `${process.env.FRONTEND_URL}/api/linkgroup/add`,
45 | {
46 | method: 'POST',
47 | body: JSON.stringify(data),
48 | }
49 | ).then((res) => res.json());
50 | return newGroup;
51 | } catch (error) {
52 | console.log('E ' + error);
53 | return false;
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/packages/linx-next/components/UserDropdown/UserDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { useUser } from '../../context/user.context';
2 | import React, { useRef, useState } from 'react';
3 | import * as S from './UserDropdown.styled';
4 | import { ChevronDown } from '../../assets/icons';
5 | import Link from 'next/link';
6 | import useOutside from '../../hooks/useOutside';
7 |
8 | const UserDropdown = () => {
9 | const { user, isAuthenticated, logout } = useUser();
10 | const [Open, setOpen] = useState(false);
11 | const ref = useRef(null);
12 | useOutside(ref, () => setOpen(false));
13 | return (
14 |
15 | setOpen(!Open)}>
16 | {isAuthenticated && user.name}
17 |
18 |
19 |
20 |
21 |
22 |
27 | Profile
28 |
29 |
30 |
31 | logout({ redirectLocation: '/' })}>
32 | Logout
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default UserDropdown;
40 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/SignIn/SignIn.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | z-index: 10;
5 | position: relative;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | flex-direction: column;
10 | min-width: 38rem;
11 | margin: 0 auto;
12 | padding: 5rem 0 3rem;
13 | border-radius: 3.3rem;
14 | background-color: ${({ theme }) => theme.backgroundSecondary};
15 |
16 | & button {
17 | width: 30rem;
18 | height: 4rem;
19 | margin-top: 2rem;
20 | }
21 |
22 | & input {
23 | margin-top: 1rem;
24 | width: 30rem;
25 | }
26 |
27 | & form label {
28 | margin-bottom: 1rem;
29 | }
30 | `;
31 |
32 | export const Title = styled.h1`
33 | margin: 1rem 0 2rem;
34 | font-family: 'Segoe UI', serif;
35 | font-size: 2.8rem;
36 | text-align: center;
37 | font-weight: bold;
38 | `;
39 |
40 | export const Input = styled.input`
41 | display: flex;
42 | align-items: center;
43 | justify-content: center;
44 | width: 30rem;
45 | height: 4rem;
46 | margin-bottom: 2rem;
47 | padding: 0.5rem 1rem;
48 | border-color: rgba(249, 249, 249, 0.25);
49 | border-width: 0.1rem;
50 | border-radius: 1rem;
51 | background: transparent;
52 | color: #f9f9f9;
53 | font-weight: bold;
54 | font-size: 1.4rem;
55 | font-family: 'Poppins', sans-serif;
56 | white-space: nowrap;
57 | `;
58 |
--------------------------------------------------------------------------------
/packages/linx-next/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Lynx",
3 | "short_name": "Lynx",
4 | "description": "Links and bookmarks sharing & managing platform",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "/android-chrome-192x192.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/android-chrome-512x512.png",
18 | "sizes": "512x512",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/maskable_icon_x192",
23 | "sizes": "196x196",
24 | "type": "image/png",
25 | "purpose": "any maskable"
26 | },
27 | {
28 | "src": "/maskable_icon_x512",
29 | "sizes": "512x512",
30 | "type": "image/png",
31 | "purpose": "any maskable"
32 | },
33 | {
34 | "src": "/maskable_icon",
35 | "sizes": "1024x1024",
36 | "type": "image/png",
37 | "purpose": "any maskable"
38 | },
39 | {
40 | "src": "/maskable_icon_x128",
41 | "sizes": "128x128",
42 | "type": "image/png",
43 | "purpose": "any maskable"
44 | }
45 | ],
46 | "start_url": "/",
47 | "scope": "/",
48 | "prefer_related_applications": false,
49 | "theme_color": "#00A8AD",
50 | "background_color": "#00A8AD",
51 | "display": "standalone"
52 | }
53 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ReviewStars/ReviewStars.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import ReviewStars from './ReviewStars';
4 |
5 | describe('ReviewStars component', () => {
6 | const onChangeMock = jest.fn();
7 |
8 | it('renders empty stars when rating is 0', () => {
9 | const { container } = render();
10 | expect(
11 | container.querySelectorAll('[data-testid="star-empty"]')
12 | ).toHaveLength(5);
13 | });
14 |
15 | it('renders full stars up to the rating', () => {
16 | const { container } = render();
17 | expect(
18 | container.querySelectorAll('[data-testid="star-full"]')
19 | ).toHaveLength(3);
20 | expect(
21 | container.querySelectorAll('[data-testid="star-half"]')
22 | ).toHaveLength(1);
23 | expect(
24 | container.querySelectorAll('[data-testid="star-empty"]')
25 | ).toHaveLength(1);
26 | });
27 |
28 | it('calls onChange when a star is clicked and isInput is true', () => {
29 | const { container } = render(
30 |
31 | );
32 | const secondStar = container.querySelectorAll(
33 | '[data-testid="star-empty"]'
34 | )[1];
35 | fireEvent.click(secondStar);
36 | expect(onChangeMock).toHaveBeenCalledWith(2);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LynxLogoDetailNoCircleSmallBox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const SvgComponent = (props) => (
4 |
23 | );
24 |
25 | export default SvgComponent;
26 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkComponent/LinkComponent.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import { Link as L } from '@prisma/client';
4 | import LinkComponent from './LinkComponent';
5 | import { incrementLinkedCount } from '../../api/linkgroup';
6 |
7 | jest.mock('../../api/link');
8 | jest.mock('../../api/linkgroup');
9 |
10 | describe('LinkComponent', () => {
11 | const mockLink = {
12 | id: '1',
13 | link: 'https://example.com',
14 | description: 'Example Link',
15 | } as L;
16 | const mockGroupId = 'group1';
17 | const mockCreatorName = 'user1';
18 | const mockGroupName = 'group1';
19 |
20 | it('renders the link description', () => {
21 | const { getByText } = render(
22 |
28 | );
29 | expect(getByText('Example Link')).toBeInTheDocument();
30 | });
31 |
32 | it('increments the linked count when the link is clicked', () => {
33 | const { getByText } = render(
34 |
40 | );
41 | fireEvent.click(getByText('Example Link'));
42 | expect(incrementLinkedCount).toHaveBeenCalledWith(mockGroupId);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app';
2 | import { NextPage } from 'next/types';
3 | import { ReactElement, ReactNode } from 'react';
4 | import { ThemeProvider } from 'styled-components';
5 | import AuthGate from '../auth/AuthGate';
6 | import { UserProvider } from '../context/user.context';
7 | import '../styles/global.scss';
8 |
9 | type NextPageWithLayout = NextPage & {
10 | //Gets per page computed layout
11 | getLayout?: (page: ReactElement) => ReactNode;
12 | //Determines if page is auth protected
13 | requireAuth?: boolean;
14 | };
15 |
16 | type AppPropsWithLayout = AppProps & {
17 | Component: NextPageWithLayout;
18 | hasAuthCookies: boolean;
19 | };
20 | const theme = {
21 | white: '#F9F9F9',
22 | primary: '#00B9AE',
23 | background: '#16181E',
24 | backgroundSecondary: '#21242D',
25 | backgroundTertiary: '#343a46',
26 | };
27 | export type CustomTheme = typeof theme;
28 | function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
29 | const getLayout = Component.getLayout || ((page) => page);
30 | return (
31 |
32 |
33 |
34 | {Component.requireAuth ? (
35 | {getLayout()}
36 | ) : (
37 | getLayout()
38 | )}
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export default CustomApp;
46 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LogoSmall.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
25 | );
26 |
27 | export default SvgComponent;
28 |
--------------------------------------------------------------------------------
/packages/linx-next/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import './breakpoints.scss';
2 |
3 | //Colors from figma
4 | $background: #16181e;
5 | $primary: #00b9ae;
6 | $white: #f9f9f9;
7 | $highlight: #21242d;
8 |
9 | html {
10 | -webkit-text-size-adjust: 100%;
11 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
12 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
13 | sans-serif;
14 | line-height: 1.5;
15 | tab-size: 4;
16 | scroll-behavior: smooth;
17 | font-size: 62.5%;
18 | height: 100%;
19 | width: 100%;
20 | font-feature-settings: 'kern';
21 | text-rendering: optimizeLegibility;
22 | color: $white;
23 | }
24 |
25 | body {
26 | background-color: $background;
27 | font-family: inherit;
28 | line-height: inherit;
29 | display: flex;
30 | flex-direction: column;
31 | margin: 0;
32 | height: 100%;
33 | width: 100%;
34 | font-size: 1.6rem;
35 | }
36 |
37 | *,
38 | *::before,
39 | *::after {
40 | margin: 0;
41 | padding: 0;
42 | box-sizing: border-box;
43 | border-width: 0;
44 | border-style: solid;
45 | border-color: currentColor;
46 | }
47 | .vvvv {
48 | margin-top: 20rem;
49 | &.active {
50 | background: red;
51 | }
52 | }
53 | ::selection {
54 | background: $primary;
55 | color: $white;
56 | }
57 |
58 | a {
59 | color: inherit;
60 | text-decoration: inherit;
61 | }
62 |
63 | .row {
64 | display: flex;
65 | flex-direction: row;
66 | }
67 |
68 | .center {
69 | align-items: center;
70 | text-align: center;
71 | justify-content: center;
72 | }
73 |
--------------------------------------------------------------------------------
/packages/linx-next/layouts/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LogoAppName from '../../components/LogoAppName';
3 | import Button from '../../components/Button';
4 | import * as S from './Header.styled';
5 | import { GithubIcon } from '../../assets/icons';
6 | import { useUser } from '../../context/user.context';
7 | import UserDropdown from '../../components/UserDropdown';
8 | import { useRouter } from 'next/router';
9 | import Link from 'next/link';
10 | import UserNav from '../UserNav';
11 |
12 | const Header = () => {
13 | const { isAuthenticated } = useUser();
14 | const router = useRouter();
15 | const handleClick = (href: string) => {
16 | router.push(process.env.FRONTEND_URL + href);
17 | };
18 | const AuthButtons = () =>
19 | router.pathname === '/signin' ? (
20 |
21 | ) : (
22 |
23 | );
24 |
25 | return (
26 |
27 |
28 |
29 | Explore
30 | Stats
31 | Top
32 | Tags
33 |
34 |
35 | {isAuthenticated && }
36 | {!isAuthenticated && }
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default Header;
48 |
--------------------------------------------------------------------------------
/packages/api/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "..\\..\\node_modules\\nx\\schemas\\project-schema.json",
3 | "sourceRoot": "packages/api/src",
4 | "projectType": "application",
5 | "targets": {
6 | "build": {
7 | "executor": "@nrwl/node:webpack",
8 | "outputs": ["{options.outputPath}"],
9 | "options": {
10 | "outputPath": "dist/packages/api",
11 | "main": "packages/api/src/main.ts",
12 | "tsConfig": "packages/api/tsconfig.app.json",
13 | "assets": ["packages/api/src/assets"]
14 | },
15 | "configurations": {
16 | "production": {
17 | "optimization": true,
18 | "extractLicenses": true,
19 | "inspect": false,
20 | "fileReplacements": [
21 | {
22 | "replace": "packages/api/src/environments/environment.ts",
23 | "with": "packages/api/src/environments/environment.prod.ts"
24 | }
25 | ]
26 | }
27 | }
28 | },
29 | "serve": {
30 | "executor": "@nrwl/node:node",
31 | "options": {
32 | "buildTarget": "api:build"
33 | }
34 | },
35 | "lint": {
36 | "executor": "@nrwl/linter:eslint",
37 | "outputs": ["{options.outputFile}"],
38 | "options": {
39 | "lintFilePatterns": ["packages/api/**/*.ts"]
40 | }
41 | },
42 | "test": {
43 | "executor": "@nrwl/jest:jest",
44 | "outputs": ["coverage/packages/api"],
45 | "options": {
46 | "jestConfig": "packages/api/jest.config.ts",
47 | "passWithNoTests": true
48 | }
49 | }
50 | },
51 | "tags": []
52 | }
53 |
--------------------------------------------------------------------------------
/packages/linx-next/components/ErrorPanel/ErrorPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './ErrorPanel.styled';
3 | import { LynxLogoDetail, NoConnectionIcon } from '../../assets/icons';
4 |
5 | interface ErrorPageProps {
6 | type: '404' | '500' | 'offline';
7 | }
8 |
9 | const ErrorPanel: React.FC = ({ type }) => {
10 | switch (type) {
11 | case '404':
12 | return (
13 |
14 |
15 | 4
16 | 0
17 | 4
18 |
19 | No page found with that address.
20 |
21 | );
22 | case '500':
23 | return (
24 |
25 |
26 | 5
27 | 0
28 | 0
29 |
30 | Server-side error occurred.
31 |
32 | );
33 | case 'offline':
34 | return (
35 |
36 |
37 |
38 |
41 |
42 |
43 |
44 | Page not cached or Lynx is offline.
45 |
46 | Reconnect and refresh the app.
47 |
48 |
49 | );
50 | default:
51 | return <>>;
52 | }
53 | };
54 |
55 | export default ErrorPanel;
56 |
--------------------------------------------------------------------------------
/packages/linx-next/next.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const withNx = require('@nrwl/next/plugins/with-nx');
3 | const withPWA = require('next-pwa')
4 |
5 | const env = process.env.NODE_ENV
6 | const isProdction = env === "production";
7 | const API_URL = isProdction ? process.env.API_URL : "http://localhost:80/"
8 | const FRONTEND_URL = isProdction ? process.env.FRONTEND_URL : "http://localhost:4200/"
9 |
10 |
11 | /**
12 | * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
13 | **/
14 | const nextConfig = {
15 | env: {
16 | API_URL,
17 | FRONTEND_URL,
18 | },
19 | async rewrites() {
20 | return [
21 | {
22 | source: '/api/:path*',
23 | destination: API_URL + ':path*'
24 | }
25 | ]
26 | },
27 | async redirects() {
28 | return [
29 | {
30 | source: '/signin/google',
31 | destination: API_URL + 'auth/signin/google',
32 | permanent: true,
33 | },
34 | {
35 | source: '/signin/github',
36 | destination: API_URL + 'auth/signin/github',
37 | permanent: true,
38 | }
39 | ]
40 | },
41 | nx: {
42 | // Set this to true if you would like to to use SVGR
43 | // See: https://github.com/gregberge/svgr
44 | svgr: true,
45 | swcMinify: true,
46 | reactStrictMode: true,
47 | compiler: {
48 | styledComponents: true,
49 | },
50 |
51 |
52 | },
53 | pwa: {
54 | dest: "public",
55 | register: true,
56 | skipWaiting: true,
57 | disable: !isProdction
58 | },
59 | };
60 |
61 | module.exports = withNx(withPWA(nextConfig));
62 |
--------------------------------------------------------------------------------
/packages/linx-next/api/user.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const signIn = async ({ email, password }) => {
4 | try {
5 | const res = await axios.post(
6 | `/api/auth/signin`,
7 | {
8 | email,
9 | password,
10 | },
11 | { withCredentials: true }
12 | );
13 | console.log(res.data);
14 | return res;
15 | } catch (error) {
16 | console.log('E ' + error);
17 | }
18 | };
19 |
20 | export const doLogout = async () => {
21 | try {
22 | await axios
23 | .get(`${process.env.FRONTEND_URL}/api/auth/logout`, {
24 | withCredentials: true,
25 | })
26 | .then((r) => r.data);
27 | } catch (error) {
28 | console.log(error);
29 | throw error;
30 | }
31 | };
32 |
33 | export const getUser = async () => {
34 | try {
35 | const user = await axios
36 | .get(`/api/auth/me`, { withCredentials: true })
37 | .then((r) => r.data);
38 | return user;
39 | } catch (error) {
40 | console.log(error);
41 | throw error;
42 | }
43 | };
44 |
45 | export const logout = async () => {
46 | try {
47 | await axios.get(`/api/auth/logout`);
48 | } catch (error) {
49 | console.log(error);
50 | }
51 | };
52 |
53 | export const signUp = async ({ name, email, password, repeat_password }) => {
54 | try {
55 | const res = await axios.post(
56 | `api/auth/signup`,
57 | {
58 | name,
59 | email,
60 | password,
61 | repeat_password,
62 | },
63 | { withCredentials: true }
64 | );
65 | return res;
66 | } catch (error) {
67 | console.log(error);
68 | return false;
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/packages/api/src/app/middlewares/auth/deserializeUser.ts:
--------------------------------------------------------------------------------
1 | import { defaultRouteMiddlewareInterface } from '../../../interfaces/index';
2 | import { cookieOptions } from '../../helpers/cookie';
3 | import { verifyJwt } from '../../helpers/jwt';
4 | import log from '../../helpers/logger';
5 | import { tokenRefresh } from '../../services/session';
6 |
7 | const deserializeUser: defaultRouteMiddlewareInterface = async (req, res) => {
8 | try {
9 | const accessToken =
10 | req.headers['authorization']?.split(' ')[1] || req.cookies.access_token;
11 |
12 | const refreshToken = req.headers['x-refresh'] || req.cookies.refresh_token;
13 |
14 | if (!accessToken) {
15 | return;
16 | }
17 |
18 | const { decoded, expired } = verifyJwt(accessToken);
19 | if (decoded) {
20 | console.log(decoded);
21 | res.locals.id = decoded;
22 | return;
23 | }
24 |
25 | if (expired && refreshToken) {
26 | const newAccessToken = await tokenRefresh(refreshToken);
27 | log.info('[AUTH] Minted new acces token for ' + req.ip);
28 | if (newAccessToken) {
29 | res.setHeader('x-access-token', newAccessToken as string);
30 | res.setHeader('Authorization', ('Bearer ' + newAccessToken) as string);
31 | res.cookie(
32 | 'access_token',
33 | newAccessToken,
34 | 365 * 24 * 60 * 60,
35 | cookieOptions
36 | );
37 | }
38 |
39 | const result = verifyJwt(newAccessToken as string);
40 | res.locals.id = result.decoded;
41 | return;
42 | }
43 | } catch (e) {
44 | log.error(e);
45 | }
46 |
47 | return;
48 | };
49 |
50 | export default deserializeUser;
51 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/auth/signup.ts:
--------------------------------------------------------------------------------
1 | import * as bcrypt from 'bcrypt';
2 | import log from '../../helpers/logger';
3 | import { pushDiscordWebhook } from '../../helpers/pushDiscordWebhook';
4 | import {
5 | AuthProvider,
6 | findOrCreateUser,
7 | isEmailFree,
8 | } from '../../services/user';
9 | import { validateSignUp } from '../../helpers/dataValidation';
10 | import { defaultRouteHandler } from '../../../interfaces';
11 |
12 | const handleSignup: defaultRouteHandler = async (req, res) => {
13 | try {
14 | const body = await req.json();
15 | const { name, email, password, repeat_password } = body;
16 | const lynxUser = {
17 | name,
18 | email,
19 | password,
20 | repeat_password,
21 | };
22 | log.info(name);
23 |
24 | const isUserValidated = await validateSignUp(lynxUser);
25 | if (!isUserValidated) return res.status(400).end();
26 |
27 | const isEmailRegistered = await isEmailFree(email);
28 | if (!isEmailRegistered)
29 | return res.status(403).send('This email is already in database');
30 |
31 | await bcrypt.hash(password, 10).then((hash) => {
32 | lynxUser.password = hash;
33 | });
34 | await findOrCreateUser(lynxUser, AuthProvider.Local);
35 | const discordWebhookBody = {
36 | title: `Lynx new user: ${lynxUser.name}`,
37 | description: `user authorization accepted`,
38 | };
39 | pushDiscordWebhook(discordWebhookBody);
40 | res.status(200).end();
41 | } catch (e) {
42 | log.error({ err: e.message, desc: e.response.data.error_description });
43 | res.json({ err: e.message, desc: e.response.data.error_description });
44 | }
45 | };
46 | export default handleSignup;
47 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupForm/LinkGroupForm.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | z-index: 10;
5 | width: 100%;
6 | position: relative;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | flex-direction: column;
11 | margin-top: 1rem;
12 | min-width: 0;
13 | min-height: 0;
14 | & button {
15 | width: 30rem;
16 | height: 4rem;
17 | margin-top: 2rem;
18 | }
19 | `;
20 |
21 | export const Form = styled.form`
22 | padding: 2rem;
23 | border-radius: 2rem;
24 | background-color: ${({ theme }) => theme.backgroundSecondary};
25 | `;
26 |
27 | export const Input = styled.input`
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | width: 30rem;
32 | height: 4rem;
33 | margin: 0.4rem 0 1.2rem;
34 | padding: 0.5rem 1rem;
35 | border-color: rgba(249, 249, 249, 0.25);
36 | border-width: 0.1rem;
37 | border-radius: 1rem;
38 | background: transparent;
39 | color: #f9f9f9;
40 | font-weight: bold;
41 | font-size: 1.4rem;
42 | font-family: 'Poppins', sans-serif;
43 | white-space: nowrap;
44 | `;
45 |
46 | export const TextArea = styled.textarea`
47 | display: flex;
48 | align-items: center;
49 | justify-content: center;
50 | width: 30rem;
51 | max-width: 30rem;
52 | min-width: 30rem;
53 | min-height: 10rem;
54 | margin: 0.4rem 0 1.2rem;
55 | padding: 0.5rem 1rem;
56 | border-color: rgba(249, 249, 249, 0.25);
57 | border-width: 0.1rem;
58 | border-radius: 1rem;
59 | background: transparent;
60 | color: #f9f9f9;
61 | font-weight: bold;
62 | font-size: 1.4rem;
63 | font-family: 'Poppins', sans-serif;
64 | white-space: pre-wrap;
65 | `;
66 |
--------------------------------------------------------------------------------
/packages/linx-next/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourceRoot": "packages/linx-next",
3 | "projectType": "application",
4 | "targets": {
5 | "build": {
6 | "executor": "@nrwl/next:build",
7 | "outputs": ["{options.outputPath}"],
8 | "defaultConfiguration": "production",
9 | "options": {
10 | "root": "packages/linx-next",
11 | "outputPath": "dist/packages/linx-next"
12 | },
13 | "configurations": {
14 | "development": {},
15 | "production": {}
16 | }
17 | },
18 | "serve": {
19 | "executor": "@nrwl/next:server",
20 | "defaultConfiguration": "development",
21 | "options": {
22 | "buildTarget": "linx-next:build",
23 | "dev": true
24 | },
25 | "configurations": {
26 | "development": {
27 | "buildTarget": "linx-next:build:development",
28 | "dev": true
29 | },
30 | "production": {
31 | "buildTarget": "linx-next:build:production",
32 | "dev": false
33 | }
34 | }
35 | },
36 | "export": {
37 | "executor": "@nrwl/next:export",
38 | "options": {
39 | "buildTarget": "linx-next:build:production"
40 | }
41 | },
42 | "test": {
43 | "executor": "@nrwl/jest:jest",
44 | "outputs": ["coverage/packages/linx-next"],
45 | "options": {
46 | "jestConfig": "packages/linx-next/jest.config.ts",
47 | "passWithNoTests": true
48 | }
49 | },
50 | "lint": {
51 | "executor": "@nrwl/linter:eslint",
52 | "outputs": ["{options.outputFile}"],
53 | "options": {
54 | "lintFilePatterns": ["packages/linx-next/**/*.{ts,tsx,js,jsx}"]
55 | }
56 | }
57 | },
58 | "tags": []
59 | }
60 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LynxLogoDetailNoCircle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
26 | );
27 |
28 | export default SvgComponent;
29 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/tag.ts:
--------------------------------------------------------------------------------
1 | import { LinkGroup } from '@prisma/client';
2 | import log from '../helpers/logger';
3 | import { getFromCache, setExCache, deleteFromCache } from '../helpers/redis';
4 | import db from '../lib/db';
5 |
6 | export const getTags = async () => {
7 | const cachedTags = await getFromCache('allTags');
8 |
9 | if (cachedTags) {
10 | return cachedTags;
11 | }
12 | const tags = await db.tag.findMany({
13 | include: {
14 | _count: {
15 | select: {
16 | Groups: true,
17 | },
18 | },
19 | },
20 | orderBy: {
21 | Groups: {
22 | _count: 'desc',
23 | },
24 | },
25 | });
26 | setExCache('allTags', 3600, JSON.stringify(tags));
27 | return tags;
28 | };
29 | export const createTag = async (tag) => {
30 | await deleteFromCache('allTags');
31 | return await db.tag.create(tag);
32 | };
33 | export const getTagLinkGroups = async (tagName: string) => {
34 | try {
35 | const { Groups } = await db.tag.findUnique({
36 | where: {
37 | name: tagName,
38 | },
39 | select: {
40 | Groups: {
41 | select: {
42 | group: {
43 | include: {
44 | tags: true,
45 | },
46 | },
47 | },
48 | where: {
49 | group: {
50 | privacyLevel: 0,
51 | },
52 | },
53 | },
54 | },
55 | });
56 | const linkGroups: LinkGroup[] = [];
57 | for (let index = 0; index < Groups.length; index++) {
58 | const element = Groups[index].group;
59 | linkGroups.push(element);
60 | }
61 | return linkGroups;
62 | } catch (error) {
63 | log.error(error);
64 | return [];
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/packages/linx-next/components/TagList/TagList.tsx:
--------------------------------------------------------------------------------
1 | import { GroupTag, Tag } from '@prisma/client';
2 | import Link from 'next/link';
3 | import React, { useMemo } from 'react';
4 | import * as S from './TagList.styled';
5 |
6 | interface Props {
7 | tags: (Tag & { _count: { Groups: number } })[];
8 | filterTags?: GroupTag[];
9 | showCount?: boolean;
10 | selectedTags?: number[];
11 | onClickHandler?: (index: number) => void;
12 | }
13 | const TagList = ({
14 | tags,
15 | filterTags,
16 | showCount,
17 | selectedTags,
18 | onClickHandler,
19 | }: Props) => {
20 | const filterTaglist = useMemo(() => {
21 | if (!filterTags) return tags;
22 | return filterTags.map((tag) => {
23 | for (let index = 0; index < tags.length; index++) {
24 | const element = tags[index];
25 | if (tag.tagId === element.id) {
26 | return element;
27 | }
28 | }
29 | return null;
30 | });
31 | }, [tags, filterTags]);
32 |
33 | const LinkWrapper = ({ tag, i, children }) => {
34 | if (onClickHandler)
35 | return onClickHandler(i)}>{children}
;
36 | return (
37 | {children}
38 | );
39 | };
40 |
41 | return (
42 |
43 | {filterTaglist.map((tag, index) => (
44 |
48 |
49 |
50 | {tag.name} {showCount && tag._count.Groups}
51 |
52 |
53 |
54 | ))}
55 |
56 | );
57 | };
58 |
59 | export default TagList;
60 |
--------------------------------------------------------------------------------
/packages/linx-next/components/TagList/TagList.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import TagList from './TagList';
3 |
4 | describe('TagList', () => {
5 | const tags = [
6 | { id: 1, name: 'tag1', _count: { Groups: 5 } },
7 | { id: 2, name: 'tag2', _count: { Groups: 3 } },
8 | { id: 3, name: 'tag3', _count: { Groups: 1 } },
9 | ] as any[];
10 |
11 | it('renders a list of tags', () => {
12 | render();
13 | const tag1 = screen.getByText('tag1');
14 | const tag2 = screen.getByText('tag2');
15 | const tag3 = screen.getByText('tag3');
16 | expect(tag1).toBeInTheDocument();
17 | expect(tag2).toBeInTheDocument();
18 | expect(tag3).toBeInTheDocument();
19 | });
20 |
21 | it('displays tag counts when showCount is true', () => {
22 | render();
23 | const tag1 = screen.getByText('tag1 5');
24 | const tag2 = screen.getByText('tag2 3');
25 | const tag3 = screen.getByText('tag3 1');
26 | expect(tag1).toBeInTheDocument();
27 | expect(tag2).toBeInTheDocument();
28 | expect(tag3).toBeInTheDocument();
29 | });
30 |
31 | it('calls onClickHandler when a tag is clicked', () => {
32 | const onClickHandler = jest.fn();
33 | render();
34 | const tag1 = screen.getByText('tag1');
35 | const tag2 = screen.getByText('tag2');
36 | const tag3 = screen.getByText('tag3');
37 | tag1.click();
38 | tag2.click();
39 | tag3.click();
40 | expect(onClickHandler).toHaveBeenCalledTimes(3);
41 | expect(onClickHandler).toHaveBeenCalledWith(0);
42 | expect(onClickHandler).toHaveBeenCalledWith(1);
43 | expect(onClickHandler).toHaveBeenCalledWith(2);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupHeader/LinkGroupHeader.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import LinkGroupHeader from './LinkGroupHeader';
4 | import { Link as L, Review as R, LinkGroup } from '@prisma/client';
5 |
6 | type DataType = LinkGroup & {
7 | links?: Partial[];
8 | reviews?: Partial[];
9 | };
10 | describe('LinkGroupHeader', () => {
11 | const data = {
12 | id: 'one',
13 | score: 1,
14 | name: 'Test Group',
15 | linkedCount: 5,
16 | watcherCount: 10,
17 | owner: 'testuser',
18 | groupname: 'testgroup',
19 | linksCount: 1,
20 | links: [{ id: '1', link: 'http://example.com' }],
21 | privacyLevel: 1,
22 | reviews: [
23 | { id: '1', score: 4 },
24 | { id: '2', score: 5 },
25 | ],
26 | } as Partial;
27 |
28 | it('renders the component with the correct props', () => {
29 | render();
30 | expect(screen.getByText(data.name)).toBeInTheDocument();
31 | expect(screen.getByText(`${data.owner}`)).toBeInTheDocument();
32 | expect(screen.getByText(`${data.name}`)).toBeInTheDocument();
33 | expect(screen.getByText(data.linksCount.toString())).toBeInTheDocument();
34 | expect(screen.getByText(data.linkedCount.toString())).toBeInTheDocument();
35 | expect(screen.getByText(data.watcherCount.toString())).toBeInTheDocument();
36 | expect(screen.getByText(`(${data.reviews.length})`)).toBeInTheDocument();
37 | });
38 |
39 | it('calculates the correct score and displays it', () => {
40 | render();
41 | expect(screen.getAllByTestId('star-full')).toHaveLength(4);
42 | expect(screen.getAllByTestId('star-half')).toHaveLength(1);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LynxLogoDetail.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
27 | );
28 |
29 | export default SvgComponent;
30 |
--------------------------------------------------------------------------------
/packages/api/src/app/helpers/authorizeAndEnd.ts:
--------------------------------------------------------------------------------
1 | import { User } from '@prisma/client';
2 | import { createSession } from '../services/session';
3 | import {
4 | Request,
5 | Response,
6 | DefaultRequestLocals,
7 | DefaultResponseLocals,
8 | } from 'hyper-express';
9 | import { signJwt } from './jwt';
10 | import { cookieOptions, refreshCookieOptions } from './cookie';
11 | import { AuthProvider } from '../services/user';
12 |
13 | const env = process.env.NODE_ENV;
14 |
15 | const isProduction = env === 'production';
16 |
17 | //Helper function for getting and setting user tokens
18 | export async function authorizeAndEnd(
19 | user: User,
20 | req: Request,
21 | res: Response,
22 | authProvider: AuthProvider,
23 | isLocal?: boolean
24 | ) {
25 | //Create new session
26 | const session = await createSession(
27 | user.id,
28 | req.get('user-agent') || 'No agent detected',
29 | req.ip,
30 | authProvider
31 | );
32 |
33 | //Create access & refresh tokens
34 |
35 | const access_token = signJwt(
36 | { user: user.id, session: session.id },
37 | { expiresIn: '15m' }
38 | );
39 |
40 | const refresh_token = signJwt(
41 | { user: user.id, session: session.id },
42 | { expiresIn: '1y' }
43 | );
44 |
45 | //Set user cookies
46 | res.cookie('access_token', access_token, 365 * 24 * 60 * 60, cookieOptions);
47 |
48 | res.cookie(
49 | 'refresh_token',
50 | refresh_token,
51 | 365 * 24 * 60 * 60,
52 | refreshCookieOptions
53 | );
54 |
55 | //Redirect to webapp if not credential authorization.
56 | if (!isLocal)
57 | return res
58 | .status(302)
59 | .redirect(
60 | isProduction ? process.env.FRONTEND_URL : 'http://localhost:4200/'
61 | );
62 |
63 | //Or if local just end
64 | return res.status(200).end();
65 | }
66 |
--------------------------------------------------------------------------------
/packages/linx-next/components/UserDropdown/UserDropdown.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const RelativeContainer = styled.div`
4 | position: relative;
5 | margin-left: 2rem;
6 | `;
7 | export const Container = styled.div`
8 | display: flex;
9 | flex-direction: row;
10 | align-items: center;
11 | justify-content: center;
12 | align-content: center;
13 | cursor: pointer;
14 | & > svg {
15 | margin-left: 0.5rem;
16 | margin-top: 0.25rem;
17 | }
18 | `;
19 |
20 | export const DropdownContainer = styled.div`
21 | display: none;
22 | position: absolute;
23 | min-width: 10rem;
24 | top: 4rem;
25 | right: -0.1rem;
26 | padding: 0.5rem 1rem;
27 | border: 1px solid #f9f9f9;
28 | border-radius: 0.75rem;
29 | background-color: ${({ theme }) => theme.backgroundSecondary};
30 | text-align: start;
31 |
32 | &.open {
33 | display: block;
34 | }
35 |
36 | &::before,
37 | &::after {
38 | content: '';
39 | position: absolute;
40 |
41 | left: auto;
42 | right: 0.2rem;
43 | height: 0;
44 | width: 0;
45 | pointer-events: none;
46 | }
47 |
48 | &::after {
49 | top: -1.85rem;
50 | right: 0.1rem;
51 | border: 1.1rem solid transparent;
52 | border-bottom-color: ${({ theme }) => theme.backgroundSecondary};
53 | }
54 |
55 | &::before {
56 | top: -1.95rem;
57 | /* transform: rotateZ(45deg); */
58 | border: 1rem solid transparent;
59 | border-bottom-color: #f9f9f9;
60 |
61 | /* z-index: -1; */
62 | }
63 | `;
64 |
65 | export const DropdownDivider = styled.div`
66 | height: 1px;
67 | width: 100%;
68 | background-color: rgba(249, 249, 249, 0.25);
69 | `;
70 |
71 | export const DropDownLink = styled.div`
72 | &:hover {
73 | background-color: ${({ theme }) => theme.primary};
74 | }
75 | padding: 0.5rem;
76 | cursor: pointer;
77 | `;
78 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/GithubIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
18 | );
19 |
20 | export default SvgComponent;
21 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/SignUp/SignUp.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | z-index: 10;
5 | position: relative;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | flex-direction: row;
10 | min-width: 60rem;
11 | margin: 1rem auto 3rem;
12 | padding: 4rem 0;
13 | border-radius: 3.3rem;
14 | background-color: ${({ theme }) => theme.backgroundSecondary};
15 |
16 | & button {
17 | width: 30rem;
18 | height: 4rem;
19 | margin-top: 2rem;
20 | }
21 | `;
22 |
23 | export const Title = styled.h1`
24 | margin: 1.5rem 0;
25 | font-family: 'Segoe UI', serif;
26 | font-size: 2.8rem;
27 | text-align: center;
28 | font-weight: bold;
29 | `;
30 |
31 | export const Column = styled.div`
32 | display: flex;
33 | align-items: center;
34 | justify-content: center;
35 | flex-direction: column;
36 | width: 100%;
37 | padding: 0 2rem;
38 | &:nth-child(1) {
39 | padding-right: 0.5rem;
40 | }
41 | &:nth-child(2) {
42 | padding-left: 0.5rem;
43 | }
44 | `;
45 |
46 | export const Input = styled.input`
47 | display: flex;
48 | align-items: center;
49 | justify-content: center;
50 | width: 30rem;
51 | height: 4rem;
52 | margin: 0.4rem 0 1.2rem;
53 | padding: 0.5rem 1rem;
54 | border-color: rgba(249, 249, 249, 0.25);
55 | border-width: 0.1rem;
56 | border-radius: 1rem;
57 | background: transparent;
58 | color: #f9f9f9;
59 | font-weight: bold;
60 | font-size: 1.4rem;
61 | font-family: 'Poppins', sans-serif;
62 | white-space: nowrap;
63 | `;
64 |
65 | export const LogoContainer = styled.div`
66 | display: flex;
67 | align-items: center;
68 | justify-content: center;
69 | flex-direction: column;
70 | `;
71 |
72 | export const Info = styled.div`
73 | width: calc(100% - 6rem);
74 | margin-bottom: 1rem;
75 | font-size: 1.8rem;
76 | font-family: Inter, serif;
77 | text-align: center;
78 | font-weight: 400;
79 | `;
80 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/WatchersIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
17 | );
18 |
19 | export default SvgComponent;
20 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkComponent/LinkComponent.tsx:
--------------------------------------------------------------------------------
1 | import { Link as L } from '@prisma/client';
2 | import Link from 'next/link';
3 | import React, { useState } from 'react';
4 | import * as S from './LinkComponent.styled';
5 | import { incrementLinkedCount } from '../../api/linkgroup';
6 | import { OpenInNewTab, Trash } from '../../assets/icons';
7 | import { removeLink } from '../../api/link';
8 | import { revalidate } from '../../api/revalidate';
9 | interface Props {
10 | link: L;
11 | groupId: string;
12 | creatorName: string;
13 | groupName: string;
14 | isUserResource?: boolean;
15 | }
16 | const LinkComponent = ({
17 | link,
18 | groupId,
19 | creatorName,
20 | groupName,
21 | isUserResource,
22 | }: Props) => {
23 | const [isDisplayed, setDisplay] = useState(true);
24 |
25 | const handleLinkRemove = async () => {
26 | const res = await removeLink(link.id);
27 | if (res.status === 200) setDisplay(false);
28 |
29 | await revalidate(`/u/${creatorName}`);
30 | await revalidate(`/u/${creatorName}/${groupName}`);
31 | };
32 | if (!isDisplayed) return null;
33 | return (
34 |
35 |
36 |
37 | incrementLinkedCount(groupId)}>
38 | {link.description}
39 |
40 |
41 | {
45 | e.stopPropagation();
46 | incrementLinkedCount(groupId);
47 | }}
48 | rel="noreferrer"
49 | >
50 |
51 |
52 | {isUserResource && (
53 | {
56 | e.stopPropagation();
57 | handleLinkRemove();
58 | }}
59 | >
60 |
61 |
62 | )}
63 |
64 | );
65 | };
66 |
67 | export default LinkComponent;
68 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/u/[user].tsx:
--------------------------------------------------------------------------------
1 | import MainLayout from '../../layouts/MainLayout';
2 | import React, { ReactElement } from 'react';
3 | import MainFeed from '../../containers/MainFeed';
4 | import { GroupTag, LinkGroup, Tag } from '@prisma/client';
5 | import LynxInfoPanel from '../../components/LynxInfoPanel';
6 | import { getTags } from '../../api/tag';
7 |
8 | interface Props {
9 | initialLinkGroups: (LinkGroup & {
10 | tags: GroupTag[];
11 | _count: { links: number };
12 | })[];
13 | tags: (Tag & { _count: { Groups: number } })[];
14 | user?: string
15 | }
16 | const UserDashboard = ({ initialLinkGroups, tags, user }: Props) => {
17 | return (
18 | <>
19 | {initialLinkGroups?.length === 0 && (
20 |
21 | )}
22 |
31 | >
32 | );
33 | };
34 | export async function getStaticPaths() {
35 | const users = (await fetch(`${process.env.FRONTEND_URL}api/user/all`).then(
36 | (res) => res.json()
37 | )) as { username: string }[];
38 |
39 | const paths = users.map((usr) => ({
40 | params: { user: usr.username },
41 | }));
42 |
43 | return {
44 | paths,
45 | fallback: true,
46 | };
47 | }
48 | export async function getStaticProps(context) {
49 | const { user } = context.params;
50 | const tags = await getTags();
51 |
52 | const res = await fetch(`${process.env.FRONTEND_URL}api/user/${user}`).then(
53 | (res) => res.json()
54 | );
55 | if (res === null) {
56 | return {
57 | props: { initialLinkGroups: null },
58 | };
59 | } else {
60 | const { linkGroups } = res;
61 | return {
62 | props: { initialLinkGroups: linkGroups, tags, user },
63 | };
64 | }
65 | }
66 | UserDashboard.getLayout = (page: ReactElement) => {
67 | return {page};
68 | };
69 | export default UserDashboard;
70 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/review/index.ts:
--------------------------------------------------------------------------------
1 | import { Review } from '@prisma/client';
2 | import { authorizedRouteHandler } from '../../../interfaces';
3 | import log from '../../helpers/logger';
4 | import { pushDiscordWebhook } from '../../helpers/pushDiscordWebhook';
5 | import { createReview, deleteReview } from '../../services/review';
6 | import { getUserById } from '../../services/user';
7 | const handleReviewAdd: authorizedRouteHandler = async (req, res) => {
8 | try {
9 | const body = (await req.json()) as Omit;
10 |
11 | //Check user
12 | const usrId = res.locals.id.user;
13 | const user = await getUserById(usrId);
14 | if (user && body.creatorName === user.username) {
15 | const review = await createReview(body);
16 | if (!review) return res.status(404).end();
17 |
18 | const discordWebhookBody = {
19 | title: `Created new review | creatorName: ${body.creatorName}`,
20 | description: `with score: ${body.score} | description: ${body.description}`,
21 | };
22 | pushDiscordWebhook(discordWebhookBody);
23 |
24 | res.status(200).json(review);
25 | } else {
26 | return res.status(403).end();
27 | }
28 | } catch (e) {
29 | log.error({ err: e.message, desc: e });
30 | return res.status(500).json({ err: e.message, desc: e });
31 | }
32 | };
33 | const handleReviewDelete: authorizedRouteHandler = async (req, res) => {
34 | try {
35 | const id = req.params.id;
36 | const usrId = res.locals.id.user;
37 | const user = await getUserById(usrId);
38 |
39 | const review = await deleteReview(id, user.username);
40 | if (!review) return res.status(404).end();
41 |
42 | const discordWebhookBody = {
43 | title: `Review deleted | id: ${id}`,
44 | description: `--==--`,
45 | };
46 | pushDiscordWebhook(discordWebhookBody);
47 |
48 | res.status(200).json({ success: true });
49 | } catch (e) {
50 | log.error({ err: e.message, desc: e });
51 | return res.status(500).json({ err: e.message, desc: e });
52 | }
53 | };
54 |
55 | export { handleReviewAdd, handleReviewDelete };
56 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupHeader/LinkGroupHeader.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { LinkIcon, Lock } from '../../assets/icons';
3 |
4 | export const Header = styled.div`
5 | display: flex;
6 | align-items: flex-start;
7 | justify-content: space-between;
8 | width: 100%;
9 | min-height: 10rem;
10 | padding: 3rem 2rem;
11 | border-radius: 2rem 2rem 0 0;
12 | background: ${({ theme }) => theme.backgroundSecondary};
13 | `;
14 |
15 | export const LinkIco = styled(LinkIcon)`
16 | transform: rotate(-45deg);
17 | `;
18 |
19 | export const LockIco = styled(Lock)`
20 | height: 3rem;
21 | width: 3rem;
22 | margin-left: 0.5rem;
23 | transform: translateY(0.1rem);
24 | opacity: 0.3;
25 | color: ${({ theme }) => theme.white};
26 | `;
27 |
28 | export const HeaderLeftPart = styled.div`
29 | display: flex;
30 | align-items: flex-start;
31 | justify-content: flex-start;
32 | flex-direction: column;
33 | `;
34 |
35 | export const StatsWrapper = styled.div`
36 | display: flex;
37 | align-items: center;
38 | justify-content: center;
39 | margin-bottom: 1.4rem;
40 | & > div:not(:first-child) {
41 | margin-left: 1.4rem;
42 | }
43 | `;
44 |
45 | export const HeaderRightPart = styled.div`
46 | display: flex;
47 | align-items: flex-end;
48 | justify-content: flex-end;
49 | flex-direction: column;
50 | `;
51 |
52 | export const TitleWrapper = styled.div`
53 | display: flex;
54 | align-items: center;
55 | justify-content: flex-start;
56 | margin-bottom: 0.6rem;
57 |
58 | & > a {
59 | display: inline-block;
60 | font-family: 'Segoe UI', serif;
61 | text-align: center;
62 | overflow: hidden;
63 | text-overflow: ellipsis;
64 | white-space: nowrap;
65 | }
66 | & > a:nth-child(1) {
67 | font-weight: 200;
68 | font-size: 3rem;
69 | max-width: 15rem;
70 | }
71 | & > a:nth-child(3) {
72 | font-weight: bold;
73 | font-size: 3rem;
74 | max-width: 32rem;
75 | }
76 | `;
77 |
78 | export const TitleDivider = styled.div`
79 | font-family: 'Segoe UI', serif;
80 | text-align: center;
81 | font-weight: normal;
82 | font-size: 2.5rem;
83 | `;
84 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupHeader/LinkGroupHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import * as S from './LinkGroupHeader.styled';
4 | import { Link as L, Review as R, LinkGroup } from '@prisma/client';
5 | import StatPill from '../StatPill';
6 | import ReviewStars from '../ReviewStars';
7 | import { LinkedAmountIcon, WatchersIcon } from '../../assets/icons';
8 |
9 | interface Props {
10 | data: LinkGroup & {
11 | links?: L[];
12 | reviews?: R[];
13 | };
14 | }
15 |
16 | const LinkGroupHeader: React.FC = ({ data }) => {
17 | const {
18 | name,
19 | linkedCount,
20 | watcherCount,
21 | owner,
22 | groupname,
23 | linksCount,
24 | links,
25 | privacyLevel,
26 | reviews,
27 | } = data;
28 |
29 | const getReviewsCount = () => {
30 | if (reviews) return reviews.length;
31 | return 0;
32 | };
33 | const getScore = () => {
34 | if (!reviews) return 0;
35 | return (
36 | reviews.reduce((sum, review) => sum + review.score, 0) / getReviewsCount()
37 | );
38 | };
39 | const getLinksCount = () => {
40 | if (links) return links.length;
41 | return linksCount || 0;
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 | {owner}
49 | /
50 | {name}
51 | {privacyLevel === 6 && }
52 |
53 |
54 |
55 |
56 | } />
57 | } />
58 | } />
59 |
60 | }
63 | isReversed={true}
64 | />
65 |
66 |
67 | );
68 | };
69 |
70 | export default LinkGroupHeader;
71 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/session.ts:
--------------------------------------------------------------------------------
1 | import { signJwt, verifyJwt } from '../helpers/jwt';
2 | import log from '../helpers/logger';
3 | import { deleteFromCache, getFromCache, setExCache } from '../helpers/redis';
4 | import db from '../lib/db';
5 | import { AuthProvider, getUserById } from './user';
6 |
7 | export async function createSession(
8 | userId: string,
9 | userAgent: string,
10 | ip: string,
11 | authProvider: AuthProvider
12 | ) {
13 | const session = await db.session.create({
14 | data: {
15 | userId,
16 | userAgent,
17 | ip,
18 | authProvider,
19 | },
20 | });
21 | setExCache(session.id, 9000, JSON.stringify(session));
22 | return session;
23 | }
24 |
25 | export async function removeSession(sessionId: string) {
26 | try {
27 | deleteFromCache(sessionId);
28 | await db.session.delete({ where: { id: sessionId } });
29 | } catch (e) {
30 | log.error(e);
31 | }
32 | }
33 |
34 | export async function removeAllSessions(userId: string) {
35 | await db.session.deleteMany({ where: { userId } });
36 | }
37 |
38 | export async function findSession(sessionId: string) {
39 | const cachedResponse = await getFromCache(sessionId);
40 | if (cachedResponse) {
41 | return cachedResponse;
42 | } else {
43 | const session = await db.session.findUnique({
44 | where: {
45 | id: sessionId,
46 | },
47 | });
48 | setExCache(sessionId, 9000, JSON.stringify(session));
49 | return session;
50 | }
51 | }
52 |
53 | export async function updateSession(sessionId: string, data) {
54 | return db.session.update({
55 | where: {
56 | id: sessionId,
57 | },
58 | data,
59 | });
60 | }
61 |
62 | export async function tokenRefresh(refresh_token: string) {
63 | const { decoded } = verifyJwt(refresh_token);
64 | const session = await findSession(decoded.session);
65 |
66 | if (!session || !session.valid) return false;
67 |
68 | const user = await getUserById(decoded.user);
69 |
70 | if (!user) return false;
71 | const accessToken = signJwt(
72 | { user: user.id, session: session.id },
73 | { expiresIn: '15m' }
74 | );
75 |
76 | return accessToken;
77 | }
78 |
--------------------------------------------------------------------------------
/packages/api/src/app/services/link.ts:
--------------------------------------------------------------------------------
1 | import db from '../lib/db';
2 | import {
3 | hideObjectKeysWithoutValues,
4 | hideSelectedObjectKeys,
5 | } from '../helpers/utilsJS';
6 | import { deleteFromCache, setExCache } from '../helpers/redis';
7 | import log from "../helpers/logger";
8 |
9 | export async function createLink(link) {
10 | try {
11 | link = hideSelectedObjectKeys(link, ['id', 'stars']);
12 | const newLink = await db.link.create({
13 | data: {
14 | ...link,
15 | stars: 0,
16 | },
17 | });
18 | //Cache after create
19 | // setExCache(newLink.id, 3600, JSON.stringify(newLink));
20 | return newLink;
21 | } catch (e) {
22 | log.error(e);
23 | return false;
24 | }
25 | }
26 |
27 | export async function editLinkInDatabase(updatedLink, linkId) {
28 | try {
29 | updatedLink = hideSelectedObjectKeys(updatedLink, ['owner', 'id']);
30 | updatedLink = hideObjectKeysWithoutValues(updatedLink);
31 | const linkFromDb = await db.link.update({
32 | data: updatedLink,
33 | where: {
34 | id: linkId,
35 | },
36 | });
37 | //Cache after edit
38 | // setExCache(linkFromDb.id, 3600, JSON.stringify(linkFromDb));
39 | if (!linkFromDb) return null;
40 | return linkFromDb;
41 | } catch (e) {
42 | log.error(e);
43 | return false;
44 | }
45 | }
46 |
47 | export async function deleteLinkFromDatabase(linkId) {
48 | try {
49 | await db.link.delete({
50 | where: {
51 | id: linkId,
52 | },
53 | });
54 | // deleteFromCache(linkId);
55 | return true;
56 | } catch {
57 | return false;
58 | }
59 | }
60 |
61 | export async function getLinkFromDatabase(linkId) {
62 | try {
63 | const linkFromDb = await db.link.findFirst({
64 | where: {
65 | id: linkId,
66 | },
67 | });
68 | if (!linkFromDb) return null;
69 | return linkFromDb;
70 | } catch (e) {
71 | log.error(e);
72 | return false;
73 | }
74 | }
75 |
76 | export async function getLinksFromDatabase(limit, page) {
77 | try {
78 | const linksFromDb = await db.link.findMany({
79 | skip: limit * page,
80 | take: limit,
81 | });
82 | if (!linksFromDb) return null;
83 | return linksFromDb;
84 | } catch (e) {
85 | log.error(e);
86 | return false;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/packages/api/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Server } from 'hyper-express';
2 | import 'dotenv';
3 | import {
4 | authRouter,
5 | linkGroupRouter,
6 | linkRouter,
7 | tagRouter,
8 | userGroupRouter,
9 | userRouter,
10 | } from './app/routes';
11 | import log from './app/helpers/logger';
12 | import * as cookieParser from 'cookie-parser';
13 | import deserializeUser from './app/middlewares/auth/deserializeUser';
14 | import rateLimiterMiddleware from './app/middlewares/rateLimit';
15 | import * as cors from 'cors';
16 | import statRouter from './app/routes/stats';
17 | import { measureRequest } from './app/middlewares/measureRequest';
18 | import reviewRouter from './app/routes/review';
19 | import redisClient from './app/lib/redis';
20 | const { FRONTEND_URL, NODE_ENV } = process.env;
21 | const isProduction = NODE_ENV === 'production';
22 | const app = new Server();
23 |
24 | // Create GET route to serve 'Hello World'
25 | app.get('/hello', (request, response) => {
26 | log.info('HELLO');
27 | response.send('Hello World');
28 | });
29 |
30 | //If user requests server favicon
31 | app.get('/favicon.ico', (req, res) => {
32 | res.sendFile(__dirname + '/assets/favicon.ico');
33 | });
34 |
35 | //Heatlhcheck route for checking if service is online
36 | app.get('/healthcheck', (req, res) => {
37 | res.status(200).end();
38 | });
39 | //Handle all of unsuported routes
40 | app.get('/*', (req, res) => {
41 | res.status(404).send('Unsupported route');
42 | });
43 |
44 | const port = process.env.PORT || 80;
45 |
46 | app.use(cors({ credentials: true, origin: isProduction ? FRONTEND_URL : '*' }));
47 | app.use(cookieParser());
48 | app.use(deserializeUser);
49 | app.use(rateLimiterMiddleware);
50 | if (!isProduction) {
51 | app.use(measureRequest);
52 | }
53 |
54 | app.use('/auth', authRouter);
55 | app.use('/user', userRouter);
56 | app.use('/stats', statRouter);
57 | app.use('/usersgroup', userGroupRouter);
58 | app.use('/link', linkRouter);
59 | app.use('/linkgroup', linkGroupRouter);
60 | app.use('/tag', tagRouter);
61 | app.use('/review', reviewRouter);
62 |
63 | app
64 | .listen(port as number)
65 | .then(async () => {
66 | log.info('[START] LYNX API ONLINE: ' + port);
67 | try {
68 | await redisClient.connect();
69 | } catch (e) {
70 | log.info('[REDIS] ' + e);
71 | }
72 | })
73 | .catch((error) =>
74 | log.error('FAILED TO START API: ' + port + ' Error ' + error)
75 | );
76 |
--------------------------------------------------------------------------------
/packages/api/src/app/controllers/auth/google.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AuthProvider,
3 | findOrCreateUser,
4 | getGoogleOAuthTokens,
5 | getGoogleUser,
6 | } from '../../services/user';
7 | import { pushDiscordWebhook } from '../../helpers/pushDiscordWebhook';
8 | import { authorizeAndEnd } from '../../helpers/authorizeAndEnd';
9 | import { defaultRouteHandler } from '../../../interfaces';
10 | import log from '../../helpers/logger';
11 | const { GOOGLE_APP_ID, FRONTEND_URL } = process.env;
12 |
13 | const env = process.env.NODE_ENV;
14 | const isDev = env === 'development';
15 |
16 | class GoogleAuthController {
17 | public oauthRedirect: defaultRouteHandler = async (req, res) => {
18 | const url = 'https://accounts.google.com/o/oauth2/v2/auth';
19 | const body = {
20 | redirect_uri: `${
21 | isDev ? 'http://localhost:4200/' : FRONTEND_URL
22 | }api/auth/signin/google/callback`,
23 | client_id: GOOGLE_APP_ID,
24 | access_type: 'offline',
25 | response_type: 'code',
26 | prompt: 'consent',
27 | scope: [
28 | 'https://www.googleapis.com/auth/userinfo.profile',
29 | 'https://www.googleapis.com/auth/userinfo.email',
30 | ].join(' '),
31 | };
32 | const qs = new URLSearchParams(body);
33 | res.redirect(`${url}?${qs}`);
34 | };
35 | public oauthCallback: defaultRouteHandler = async (req, res) => {
36 | const code = req.query.code as string;
37 | try {
38 | const tokenBundle = await getGoogleOAuthTokens(code);
39 |
40 | console.log(tokenBundle);
41 |
42 | //Get google user from GoogleAPI || also possible to decode from token
43 | const googleUser = await getGoogleUser(
44 | tokenBundle.id_token,
45 | tokenBundle.access_token
46 | );
47 |
48 | console.log(googleUser);
49 | const discordWebhookBody = {
50 | title: `Google new user: ${googleUser.email}`,
51 | description: `user authorization accepted`,
52 | };
53 | pushDiscordWebhook(discordWebhookBody);
54 | //TODO add user to database, forward token data to frontend
55 | const user = await findOrCreateUser(googleUser, AuthProvider.Google);
56 |
57 | return authorizeAndEnd(user, req, res, AuthProvider.Google);
58 | } catch (e) {
59 | log.info(e);
60 | res.json({ err: e.message, desc: 'Google login failed' });
61 | }
62 | };
63 | }
64 |
65 | export default GoogleAuthController;
66 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/NoConnectionIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { SVGProps } from "react"
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
76 | )
77 |
78 | export default SvgComponent
79 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/u/[user]/[group].tsx:
--------------------------------------------------------------------------------
1 | import MainLayout from '../../../layouts/MainLayout';
2 | import React, { ReactElement, useEffect, useState } from 'react';
3 | import { GroupTag, Link, LinkGroup, Review, Tag, User } from '@prisma/client';
4 | import { GetStaticPaths, GetStaticProps } from 'next';
5 | import LinkGroupDisplay from '../../../components/LinkGroupDisplay';
6 | import { getTags } from '../../../api/tag';
7 |
8 | interface Props {
9 | groupWithLinks: LinkGroup & {
10 | tags: GroupTag[];
11 | links: Link[];
12 | reviews: Review[];
13 | };
14 | tags: (Tag & { _count: { Groups: number } })[];
15 | }
16 | const ShowGroupContent = ({ groupWithLinks, tags }: Props) => {
17 | const [linksGroup, updateLinksGroup] = useState(groupWithLinks);
18 | const addNewLinkToState = (link: Link) => {
19 | const updatedLinksGroup = { ...linksGroup };
20 | updatedLinksGroup.links.push(link);
21 | updateLinksGroup(updatedLinksGroup);
22 | };
23 | useEffect(() => {
24 | updateLinksGroup(groupWithLinks);
25 | }, [groupWithLinks]);
26 |
27 | if (linksGroup) {
28 | return (
29 |
34 | );
35 | }
36 | return <>No data>;
37 | };
38 | export const getStaticPaths: GetStaticPaths = async () => {
39 | const users = (await fetch(
40 | `${process.env.FRONTEND_URL}api/user/all/groups`
41 | ).then((res) => res.json())) as (User & { linkGroups: LinkGroup[] })[];
42 |
43 | const paths = [];
44 | for (let i = 0; i < users.length; i++) {
45 | const user = users[i];
46 | for (let j = 0; j < user.linkGroups.length; j++) {
47 | const group = user.linkGroups[j];
48 | paths.push({ params: { user: user.username, group: group.groupname } });
49 | }
50 | }
51 | return { paths, fallback: true };
52 | };
53 | export const getStaticProps: GetStaticProps = async (context) => {
54 | // const { group } = context.params;
55 | const { user, group } = context.params;
56 |
57 | const tags = await getTags();
58 |
59 | const res = await fetch(
60 | `${process.env.FRONTEND_URL}api/user/${user}/g/${group}`
61 | ).then((res) => res.json());
62 | if (res === null) {
63 | return {
64 | props: { groupWithLinks: null },
65 | };
66 | } else {
67 | return {
68 | props: { groupWithLinks: res, tags },
69 | };
70 | }
71 | };
72 |
73 | ShowGroupContent.getLayout = (page: ReactElement) => {
74 | return {page};
75 | };
76 | export default ShowGroupContent;
77 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupForm/LinkGroupForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { useHotkeys } from 'react-hotkeys-hook';
3 | import { SubmitHandler, useForm } from 'react-hook-form';
4 | import useOutside from '../../hooks/useOutside';
5 | import * as S from './LinkGroupForm.styled';
6 | import Button from '../Button';
7 | import ExpandingButton from '../ExpandingButton';
8 | import { addLink } from '../../api/link';
9 | import { revalidate } from '../../api/revalidate';
10 | import { Link } from '@prisma/client';
11 |
12 | interface Props {
13 | groupId: string;
14 | creatorName: string;
15 | groupName: string;
16 | addNewLinkToState?: (link: Link) => void;
17 | }
18 | type Inputs = {
19 | link: string;
20 | description: string;
21 | };
22 |
23 | const LinkGroupForm: React.FC = ({
24 | groupId,
25 | addNewLinkToState,
26 | creatorName,
27 | groupName,
28 | }) => {
29 | const [isExpanded, setExpansionState] = useState(false);
30 | const ref = useRef(null);
31 | useOutside(ref, () => setExpansionState(false));
32 |
33 | const { register, handleSubmit } = useForm();
34 |
35 | //Enter key press handler => submit form
36 | useHotkeys('enter, numpadenter', () => {
37 | handleSubmit(onSubmit)();
38 | });
39 |
40 | const onSubmit: SubmitHandler = async (data) => {
41 | const { link, description } = data;
42 | const addedLink = await addLink(link, description, 0, groupId);
43 | if (!addedLink) return;
44 | await revalidate(`/u/${creatorName}`);
45 | await revalidate(`/u/${creatorName}/${groupName}`);
46 | addNewLinkToState(addedLink);
47 | setExpansionState(false);
48 | };
49 | const expandForm = (e) => {
50 | e.stopPropagation();
51 | setExpansionState(true);
52 | };
53 | return (
54 |
55 | {isExpanded ? (
56 |
57 |
58 |
62 |
63 |
68 |
71 |
72 | ) : (
73 |
80 | )}
81 |
82 | );
83 | };
84 |
85 | export default LinkGroupForm;
86 |
--------------------------------------------------------------------------------
/packages/linx-next/containers/SignIn/SignIn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as S from './SignIn.styled';
3 | import { SubmitHandler, useForm } from 'react-hook-form';
4 | import AuthLinkFlavor from '../../components/AuthLinkFlavor';
5 | import { LynxLogoDetail } from '../../assets/icons';
6 | import {
7 | AuthImportantText,
8 | SmallAuthText,
9 | } from '../../components/Text/Text.styled';
10 | import Button from '../../components/Button';
11 | import GithubLoginButton from '../../components/GithubLoginButton';
12 | import GoogleLoginButton from '../../components/GoogleLoginButton';
13 | import Link from 'next/link';
14 | import { useUser } from '../../context/user.context';
15 | import { useHotkeys } from 'react-hotkeys-hook';
16 | import { useRouter } from 'next/router';
17 |
18 | type Inputs = {
19 | email: string;
20 | password: string;
21 | };
22 |
23 | const SignIn = () => {
24 | const {
25 | register,
26 | handleSubmit,
27 | watch,
28 | formState: { errors },
29 | } = useForm();
30 | const router = useRouter();
31 | const { login, isAuthenticated } = useUser();
32 | //Enter key press handler => submit form
33 | useHotkeys('enter, numpadenter', () => {
34 | handleSubmit(onSubmit)();
35 | });
36 |
37 | if (isAuthenticated) {
38 | router.push('/');
39 | return null;
40 | }
41 |
42 | const onSubmit: SubmitHandler = async (data) => {
43 | login(data);
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 | Hi, welcome back!
51 |
67 |
68 |
69 |
82 |
83 | );
84 | };
85 |
86 | export default SignIn;
87 |
--------------------------------------------------------------------------------
/packages/linx-next/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/display-name */
2 | import { ReactElement } from 'react';
3 | import Document, { Html, Head, Main, NextScript } from 'next/document';
4 | import { ServerStyleSheet } from 'styled-components';
5 |
6 | export default class CustomDocument extends Document<{
7 | styleTags: ReactElement[];
8 | }> {
9 | static async getInitialProps(ctx) {
10 | const sheet = new ServerStyleSheet();
11 | const originalRenderPage = ctx.renderPage;
12 | try {
13 | ctx.renderPage = () =>
14 | originalRenderPage({
15 | enhanceApp: (App) => (props) =>
16 | sheet.collectStyles(),
17 | });
18 |
19 | const initialProps = await Document.getInitialProps(ctx);
20 |
21 | return {
22 | ...initialProps,
23 | styles: (initialProps.styles, sheet.getStyleElement()),
24 | };
25 | } finally {
26 | sheet.seal();
27 | }
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
39 |
45 |
51 |
52 |
57 |
61 |
62 |
63 |
64 |
65 |
66 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/packages/linx-next/assets/icons/LinkedAmountIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SVGProps } from 'react';
3 |
4 | const SvgComponent = (props: SVGProps) => (
5 |
17 | );
18 |
19 | export default SvgComponent;
20 |
--------------------------------------------------------------------------------
/packages/linx-next/components/LinkGroupBody/LinkGroupBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import * as S from './LinkGroupBody.styled';
3 | import { GroupTag, Link as L, LinkGroup, Review, Tag } from '@prisma/client';
4 | import LinkComponent from '../LinkComponent';
5 | import LinkGroupForm from '../LinkGroupForm';
6 | import TagList from '../TagList/';
7 | import { useUser } from '../../context/user.context';
8 | import ReviewComponent from '../ReviewComponent';
9 | import ReviewForm from '../ReviewForm';
10 | import { useRouter } from 'next/router';
11 |
12 | interface Props {
13 | data: LinkGroup & {
14 | tags: GroupTag[];
15 | links?: L[];
16 | reviews?: Review[];
17 | };
18 | tags: (Tag & { _count: { Groups: number } })[];
19 | addNewLinkToState?: (link: L) => void;
20 | }
21 |
22 | const LinkGroupBody: React.FC = ({ data, tags, addNewLinkToState }) => {
23 | const router = useRouter();
24 | const {
25 | owner,
26 | id: groupId,
27 | description,
28 | links,
29 | tags: dT,
30 | reviews,
31 | groupname: groupName,
32 | } = data;
33 | const { isUserResource, isAuthenticated, user } = useUser();
34 |
35 | const hasAlreadyReviewed = useMemo(() => {
36 | if (reviews && user && user?.username) {
37 | for (let index = 0; index < reviews.length; index++) {
38 | const review = reviews[index];
39 | if (review.creatorName === user.username) return true;
40 | }
41 | return false;
42 | }
43 | }, [reviews, user]);
44 | return (
45 |
46 |
47 |
48 |
49 | {description}
50 | {links?.map((link) => (
51 |
59 | ))}
60 | {isUserResource && (
61 |
67 | )}
68 | {router.query?.group && reviews?.length > 0 && (
69 | Reviews
70 | )}
71 | {router.query?.group &&
72 | reviews?.map((review) => (
73 |
74 | ))}
75 | {router.query?.group &&
76 | !isUserResource &&
77 | !hasAlreadyReviewed &&
78 | isAuthenticated && (
79 |
85 | )}
86 |
87 | );
88 | };
89 |
90 | export default LinkGroupBody;
91 |
--------------------------------------------------------------------------------