├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── translation_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── labeler.yml └── workflows │ ├── beta-deploy.yml │ ├── ci.yml │ ├── codeql.yml │ ├── docker-image.yml │ └── prod-deploy.yml ├── .gitignore ├── .gitpod.yml ├── .husky ├── .gitignore └── commit-msg ├── .prettierignore ├── .prettierrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-typescript.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-2.4.2.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── logos │ ├── png │ └── oasis-logo-white.png │ └── svg │ ├── icon.svg │ └── logo.svg ├── commitlint.config.js ├── docker-compose.yml ├── docker ├── .dockerenv.example └── docker-ormconfig.ts ├── package.json ├── packages ├── api │ ├── .env.example │ ├── .gitignore │ ├── __tests__ │ │ ├── BotAuth.test.ts │ │ ├── CreatePost.test.ts │ │ ├── CreateResort.test.ts │ │ ├── CurrentUser.test.ts │ │ ├── GetNotifications.test.ts │ │ ├── GetQueue.test.ts │ │ ├── MakeBadge.test.ts │ │ ├── MakeReport.test.ts │ │ ├── PaginateUsers.test.ts │ │ └── Search.test.ts │ ├── jest.config.ts │ ├── package.json │ ├── schema.gql │ ├── src │ │ ├── auth │ │ │ ├── connections │ │ │ │ ├── index.ts │ │ │ │ └── methods │ │ │ │ │ └── spotify.ts │ │ │ └── oauth │ │ │ │ ├── index.ts │ │ │ │ └── providers │ │ │ │ ├── discord.ts │ │ │ │ ├── github.ts │ │ │ │ ├── google.ts │ │ │ │ └── twitter.ts │ │ ├── build.ts │ │ ├── config │ │ │ ├── database.ts │ │ │ ├── redis.ts │ │ │ ├── s3.ts │ │ │ └── urls.ts │ │ ├── entities │ │ │ ├── Answer.ts │ │ │ ├── Badge.ts │ │ │ ├── Comment.ts │ │ │ ├── Connection.ts │ │ │ ├── Notification.ts │ │ │ ├── Post.ts │ │ │ ├── PremiumToken.ts │ │ │ ├── Question.ts │ │ │ ├── Report.ts │ │ │ ├── Resort.ts │ │ │ └── User.ts │ │ ├── enums │ │ │ ├── Notifications.ts │ │ │ ├── Reports.ts │ │ │ ├── Roles.ts │ │ │ └── Status.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── constants.ts │ │ │ └── nodeMajor.ts │ │ ├── middleware │ │ │ ├── NotBanned.ts │ │ │ ├── RateLimit.ts │ │ │ └── SelfOnly.ts │ │ ├── resolvers │ │ │ ├── answer │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── GetIsUpvotedDownvoted.resolver.ts │ │ │ │ ├── UpvoteDownvoteAnswer.resolver.ts │ │ │ │ ├── edit │ │ │ │ │ ├── EditAnswer.resolver.ts │ │ │ │ │ └── EditAnswerInput.ts │ │ │ │ └── new │ │ │ │ │ ├── NewAnswer.resolver.ts │ │ │ │ │ └── NewAnswerInput.ts │ │ │ ├── badge │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── GiveBadge.resolver.ts │ │ │ │ └── make │ │ │ │ │ ├── MakeBadge.resolver.ts │ │ │ │ │ └── MakeBadgeInput.ts │ │ │ ├── bot │ │ │ │ ├── CreateBot.resolver.ts │ │ │ │ └── RefreshToken.resolver.ts │ │ │ ├── comment │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── GetIsUpvotedDownvoted.resolver.ts │ │ │ │ ├── UpvoteDownvoteComment.resolver.ts │ │ │ │ ├── edit │ │ │ │ │ ├── EditComment.resolver.ts │ │ │ │ │ └── EditCommentInput.ts │ │ │ │ └── new │ │ │ │ │ ├── NewComment.resolver.ts │ │ │ │ │ └── NewCommentInput.ts │ │ │ ├── notification │ │ │ │ ├── GetNotifications.resolver.ts │ │ │ │ └── MarkNotificationAsRead.resolver.ts │ │ │ ├── post │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── DeletePost.resolver.ts │ │ │ │ ├── GetIsUpvotedDownvotes.resolver.ts │ │ │ │ ├── UpvoteDownvotePost.resolver.ts │ │ │ │ ├── edit │ │ │ │ │ ├── EditPost.resolver.ts │ │ │ │ │ └── EditPostInput.ts │ │ │ │ ├── feed │ │ │ │ │ └── FeedSort.resolver.ts │ │ │ │ └── new │ │ │ │ │ ├── CreatePost.resolver.ts │ │ │ │ │ └── CreatePostInput.ts │ │ │ ├── question │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── DeletePost.resolver.ts │ │ │ │ ├── GetIsUpvotedDownvoted.resolver.ts │ │ │ │ ├── edit │ │ │ │ │ ├── EditQuestion.resolver.ts │ │ │ │ │ └── EditQuestionInput.ts │ │ │ │ └── new │ │ │ │ │ ├── CreateQuestion.resolver.ts │ │ │ │ │ └── CreateQuestionInput.ts │ │ │ ├── reports │ │ │ │ ├── GetQueue.resolver.ts │ │ │ │ ├── MakeReport.resolver.ts │ │ │ │ ├── MakeReportInput.ts │ │ │ │ ├── MarkAsResolved.resolver.ts │ │ │ │ └── ReportedEntityInput.ts │ │ │ ├── resort │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── GetIsJoined.resolver.ts │ │ │ │ ├── GetResortByName.resolver.ts │ │ │ │ └── create │ │ │ │ │ ├── CreateResort.resolver.ts │ │ │ │ │ └── CreateResortInput.ts │ │ │ ├── search │ │ │ │ └── search.resolver.ts │ │ │ └── user │ │ │ │ ├── BanUser.resolver.ts │ │ │ │ ├── Base.resolver.ts │ │ │ │ ├── CurrentUser.resolver.ts │ │ │ │ ├── DeleteAccount.resolver.ts │ │ │ │ ├── GetUserByName.resolver.ts │ │ │ │ ├── JoinResort.resolver.ts │ │ │ │ ├── MakeAdmin.resolver.ts │ │ │ │ ├── follow │ │ │ │ └── FollowResolver.resolver.ts │ │ │ │ ├── premium │ │ │ │ ├── GetTokenData.resolver.ts │ │ │ │ ├── MakePremiumToken.resolver.ts │ │ │ │ └── RedeemToken.resolver.ts │ │ │ │ ├── service-auth │ │ │ │ ├── CreateTokens.resolver.ts │ │ │ │ ├── RefreshToken.resolver.ts │ │ │ │ └── TokenDataInput.ts │ │ │ │ └── update │ │ │ │ ├── UpdateProfile.resolver.ts │ │ │ │ └── UpdateProfileInput.ts │ │ ├── routes │ │ │ ├── index.ts │ │ │ └── upload.ts │ │ ├── server.ts │ │ └── utils │ │ │ ├── auth │ │ │ ├── NoBot.ts │ │ │ ├── authChecker.ts │ │ │ ├── checkUsername.ts │ │ │ └── createContext.ts │ │ │ ├── common │ │ │ ├── hasPermission.ts │ │ │ ├── http.ts │ │ │ ├── isNull.ts │ │ │ └── rootPath.ts │ │ │ ├── connection │ │ │ └── createConnection.ts │ │ │ ├── files │ │ │ ├── createResolver.ts │ │ │ └── createSchema.ts │ │ │ ├── index.ts │ │ │ ├── output │ │ │ ├── exit.ts │ │ │ └── log.ts │ │ │ ├── paginate │ │ │ ├── PaginationResponse.ts │ │ │ ├── RelationalPagination.ts │ │ │ └── RelationalPaginationResolvers.ts │ │ │ ├── testing │ │ │ ├── gql-request.ts │ │ │ └── seedDatabase.ts │ │ │ └── votes │ │ │ └── upvoteDownvoteEntity.ts │ └── tsconfig.json ├── cli │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ ├── fetchPosts.test.ts │ │ ├── followUser.test.ts │ │ ├── getCurrentUser.test.ts │ │ ├── getMyUserId.test.ts │ │ ├── getPostById.test.ts │ │ ├── getUserByName.test.ts │ │ ├── getUsersPosts.test.ts │ │ ├── getUsersUpvotedPosts.test.ts │ │ ├── helper.ts │ │ ├── post.test.ts │ │ ├── schemas │ │ │ ├── postSchema.ts │ │ │ ├── querySchema.ts │ │ │ └── userSchema.ts │ │ ├── search.test.ts │ │ └── updateProfile.test.ts │ ├── babel.config.js │ ├── examples │ │ ├── fetching-posts │ │ │ └── fetch_posts.sh │ │ └── image-rendering │ │ │ └── image-rendering.sh │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── bin │ │ │ └── oasis.ts │ │ ├── commands │ │ │ ├── deleteAccount.ts │ │ │ ├── fetchPosts.ts │ │ │ ├── follow.ts │ │ │ ├── getPostById.ts │ │ │ ├── getUserByName.ts │ │ │ ├── getUsersPosts.ts │ │ │ ├── getUsersUpvotedPosts.ts │ │ │ ├── login.ts │ │ │ ├── post.ts │ │ │ ├── profile.ts │ │ │ ├── search.ts │ │ │ └── updateProfile.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── constants.ts │ │ ├── types │ │ │ └── arguments.ts │ │ └── utils │ │ │ └── output │ │ │ ├── exit.ts │ │ │ └── log.ts │ └── tsconfig.json ├── desktop │ ├── .env.example │ ├── .gitignore │ ├── package.json │ ├── resources │ │ ├── icons │ │ │ ├── icon-light.icns │ │ │ ├── icon-light.ico │ │ │ ├── icon-light.png │ │ │ ├── icon.icns │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ └── splash │ │ │ └── splash-screen.html │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── constants.ts │ │ │ └── links.ts │ └── tsconfig.json ├── mobile │ ├── .babelrc │ ├── .gitignore │ ├── capacitor.config.json │ ├── ionic.config.json │ ├── package.json │ ├── public │ │ ├── assets │ │ │ ├── icon │ │ │ │ ├── favicon.png │ │ │ │ └── icon.png │ │ │ └── shapes.svg │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ ├── ExploreContainer.css │ │ │ └── ExploreContainer.tsx │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── Home.css │ │ │ └── Home.tsx │ │ ├── react-app-env.d.ts │ │ ├── service-worker.ts │ │ ├── serviceWorkerRegistration.ts │ │ ├── setupTests.ts │ │ └── theme │ │ │ ├── globals.css │ │ │ └── variables.css │ ├── tsconfig.json │ └── webpack.config.js ├── react-gql │ ├── .gitignore │ ├── README.md │ ├── codegen.yml │ ├── package.json │ ├── src │ │ ├── comment │ │ │ ├── createComment.gql │ │ │ ├── getPostComments.gql │ │ │ ├── getUsersComments.gql │ │ │ ├── likeComment.gql │ │ │ └── newComment.gql │ │ ├── notifications │ │ │ ├── getNotifications.gql │ │ │ └── markNotificationAsRead.gql │ │ ├── post │ │ │ ├── deletePost.gql │ │ │ ├── feedSort.gql │ │ │ ├── getPosts.gql │ │ │ ├── makePost.gql │ │ │ └── upvoteDownvote.gql │ │ ├── report.gql │ │ ├── resort │ │ │ ├── getResortByName.gql │ │ │ └── joinResort.gql │ │ ├── search.gql │ │ └── user.gql │ └── tsconfig.json ├── sdk │ ├── .gitignore │ ├── README.md │ ├── codegen.yml │ ├── package.json │ ├── src │ │ ├── base-client.ts │ │ ├── client │ │ │ ├── arguments.ts │ │ │ ├── index.ts │ │ │ ├── listAll.ts │ │ │ ├── main.ts │ │ │ └── relations.ts │ │ ├── constants.ts │ │ ├── generated │ │ │ ├── client.ts │ │ │ └── types.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── constants.ts │ │ ├── query-builder.ts │ │ ├── test-utils │ │ │ └── setup.ts │ │ ├── test.ts │ │ ├── utils │ │ │ └── output │ │ │ │ ├── exit.ts │ │ │ │ └── log.ts │ │ ├── variable-type.ts │ │ └── wrappers │ │ │ └── post.ts │ └── tsconfig.json ├── tui │ ├── .gitignore │ ├── README.md │ ├── cmd │ │ └── termoasis │ │ │ └── main.go │ ├── go.mod │ ├── go.sum │ └── internal │ │ ├── model │ │ └── model.go │ │ ├── posts │ │ ├── author │ │ │ └── render.go │ │ ├── date │ │ │ └── render.go │ │ ├── downvotes │ │ │ └── render.go │ │ ├── render.go │ │ └── upvotes │ │ │ └── render.go │ │ ├── queries │ │ └── fetchPosts.go │ │ └── utils │ │ └── date.go ├── vsc-extension │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── README.md │ ├── logo.svg │ ├── media │ │ ├── reset.css │ │ └── vscode.css │ ├── out │ │ ├── Sidebar.js │ │ ├── Sidebar.js.map │ │ ├── extension.js │ │ └── extension.js.map │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Sidebar.ts │ │ ├── extension.ts │ │ └── getNonce.ts │ ├── tsconfig.json │ ├── vsc-extension-quickstart.md │ └── webviews │ │ ├── pages │ │ └── Sidebar.tsx │ │ └── tsconfig.json └── web │ ├── .babelrc │ ├── .env.example │ ├── .gitignore │ ├── .storybook │ ├── main.js │ └── preview.js │ ├── README.md │ ├── cypress.json │ ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── main.spec.ts │ ├── plugins │ │ ├── index.js │ │ └── preprocessor.js │ ├── support │ │ └── index.js │ ├── tsconfig.json │ └── utils │ │ └── loginAsTesting.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── manifest.json │ └── static │ │ ├── DiscordLogo.svg │ │ ├── GenericUser.svg │ │ ├── Logo.svg │ │ ├── OasisLogo.png │ │ ├── OasisLogo.svg │ │ ├── badges │ │ ├── contributor-badge.svg │ │ ├── dunce-cap.svg │ │ └── verified-badge.svg │ │ ├── default-banner.png │ │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ │ ├── fonts │ │ ├── VictorMono-Regular.eot │ │ ├── VictorMono-Regular.otf │ │ ├── VictorMono-Regular.ttf │ │ └── VictorMono-Regular.woff │ │ └── hack-regular-webfont.woff2 │ ├── server │ ├── src │ │ ├── config │ │ │ └── database.ts │ │ ├── getServer.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── output │ │ │ ├── exit.ts │ │ │ └── log.ts │ │ │ └── rootPath.ts │ └── tsconfig.json │ ├── src │ ├── components │ │ ├── comment │ │ │ └── Comment.tsx │ │ ├── home │ │ │ ├── CreatePostInput.tsx │ │ │ ├── FollowUser.tsx │ │ │ ├── FollowUserSection.tsx │ │ │ ├── FriendActivity.tsx │ │ │ ├── FriendActivitySection.tsx │ │ │ ├── LeftSidebar.tsx │ │ │ ├── NewPostsSection.tsx │ │ │ ├── PostsSection.tsx │ │ │ ├── ProfileSection.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── TrendingSection.tsx │ │ ├── index.ts │ │ ├── locales │ │ │ └── LanguageSelector.tsx │ │ ├── navbar │ │ │ ├── DropdownItem.tsx │ │ │ ├── HomeTopBar.tsx │ │ │ ├── NavItem.tsx │ │ │ └── Navbar.tsx │ │ ├── notifications │ │ │ ├── FilterButton.tsx │ │ │ ├── NotificationBlock.tsx │ │ │ └── NotificationWrapper.tsx │ │ ├── post │ │ │ ├── NewPost.tsx │ │ │ └── Post.tsx │ │ ├── profile │ │ │ ├── Bio.tsx │ │ │ ├── Comments.tsx │ │ │ ├── FollowersInfo.tsx │ │ │ ├── Posts.tsx │ │ │ ├── ProfileBanner.tsx │ │ │ ├── ProfilePost.tsx │ │ │ ├── TabItem.tsx │ │ │ ├── TabMeta.tsx │ │ │ ├── TopicBadge.tsx │ │ │ ├── large │ │ │ │ └── UserCard.tsx │ │ │ └── small │ │ │ │ └── UserCard.tsx │ │ ├── resort │ │ │ ├── AvatarGroup.tsx │ │ │ ├── ResortCard.tsx │ │ │ └── ResortHeader.tsx │ │ ├── shared │ │ │ ├── AutoResizeTextArea.tsx │ │ │ ├── Button.tsx │ │ │ ├── Container.tsx │ │ │ ├── Footer.tsx │ │ │ ├── FormikInput.tsx │ │ │ ├── InfiniteScrollWrapper.tsx │ │ │ ├── Input.tsx │ │ │ ├── Loading.tsx │ │ │ └── Modal.tsx │ │ └── user │ │ │ └── User.tsx │ ├── entities │ │ ├── Answer.ts │ │ ├── Badge.ts │ │ ├── Comment.ts │ │ ├── Connection.ts │ │ ├── Notification.ts │ │ ├── Post.ts │ │ ├── PremiumToken.ts │ │ ├── Question.ts │ │ ├── Report.ts │ │ ├── Resort.ts │ │ └── User.ts │ ├── enums │ │ ├── Notifications.ts │ │ ├── Reports.ts │ │ ├── Roles.ts │ │ └── Status.ts │ ├── hooks │ │ └── useOnClickOutside.tsx │ ├── icons │ │ ├── arrows │ │ │ ├── DownArrow.tsx │ │ │ ├── RightArrow.tsx │ │ │ ├── SmallDownArrow.tsx │ │ │ ├── SmallUpArrow.tsx │ │ │ └── UpArrow.tsx │ │ ├── channel │ │ │ └── Hashtag.tsx │ │ ├── index.ts │ │ ├── other │ │ │ ├── Featured.tsx │ │ │ ├── Info.tsx │ │ │ ├── ThreeDots.tsx │ │ │ └── Trash.tsx │ │ ├── posts │ │ │ ├── Comments.tsx │ │ │ ├── Latest.tsx │ │ │ ├── Like.tsx │ │ │ └── Trending.tsx │ │ ├── sidebar │ │ │ ├── About.tsx │ │ │ ├── Bell.tsx │ │ │ ├── Friends.tsx │ │ │ ├── Home.tsx │ │ │ ├── Logout.tsx │ │ │ ├── Posts.tsx │ │ │ ├── Profile.tsx │ │ │ ├── Saved.tsx │ │ │ ├── Search.tsx │ │ │ └── Topics.tsx │ │ └── social │ │ │ └── Twitter.tsx │ ├── lib │ │ ├── apollo.ts │ │ ├── auth │ │ │ └── login.ts │ │ ├── common │ │ │ ├── getCurrentUser.ts │ │ │ └── ssrRequest.ts │ │ └── constants.ts │ ├── locales │ │ ├── BaseLanguage.ts │ │ ├── LocalesProvider.tsx │ │ ├── en │ │ │ └── index.ts │ │ └── es │ │ │ └── index.ts │ ├── pages │ │ ├── 404.tsx │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── auth │ │ │ ├── success.tsx │ │ │ └── vscode.tsx │ │ ├── home │ │ │ └── new.tsx │ │ ├── index.tsx │ │ ├── notifications.tsx │ │ ├── post │ │ │ └── [id] │ │ │ │ └── index.tsx │ │ ├── resort │ │ │ ├── [resort] │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── search.tsx │ │ ├── settings │ │ │ ├── account.tsx │ │ │ └── developers.tsx │ │ └── user │ │ │ ├── [username] │ │ │ ├── index.tsx │ │ │ └── new.tsx │ │ │ └── edit.tsx │ ├── parser │ │ ├── emoji │ │ │ ├── emojiParser.ts │ │ │ └── emojis.json │ │ ├── index.ts │ │ ├── markdown │ │ │ ├── StyledMarkdown.tsx │ │ │ ├── styles │ │ │ │ ├── StyledMarkdown.module.css │ │ │ │ ├── StyledMarkdownBio.module.css │ │ │ │ ├── StyledMarkdownPost.module.css │ │ │ │ └── declaration.d.ts │ │ │ └── themes │ │ │ │ └── OasisDark.tsx │ │ └── runner │ │ │ ├── PistonRuntimesProvider.tsx │ │ │ └── RunCode.tsx │ ├── providers │ │ ├── CustomLink.tsx │ │ └── LinkProvider.tsx │ ├── shared │ │ ├── AuthProvider.tsx │ │ └── SEO.tsx │ ├── stories │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ ├── Header.stories.tsx │ │ ├── Header.tsx │ │ ├── Introduction.stories.mdx │ │ ├── Page.stories.tsx │ │ ├── Page.tsx │ │ ├── assets │ │ │ ├── code-brackets.svg │ │ │ ├── colors.svg │ │ │ ├── comments.svg │ │ │ ├── direction.svg │ │ │ ├── flow.svg │ │ │ ├── plugin.svg │ │ │ ├── repo.svg │ │ │ └── stackalt.svg │ │ ├── button.css │ │ ├── header.css │ │ └── page.css │ ├── styles │ │ └── globals.css │ └── utils │ │ ├── common │ │ ├── hasPermission.ts │ │ ├── isNull.ts │ │ ├── request.ts │ │ └── rootPath.ts │ │ ├── format │ │ ├── date.ts │ │ ├── index.ts │ │ ├── json.ts │ │ └── number.ts │ │ ├── redirect.ts │ │ ├── require.ts │ │ ├── sentry.ts │ │ └── status │ │ ├── codes.ts │ │ └── send.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── scripts ├── package.json └── setup.js ├── status └── error.html └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # yarn v2 2 | .yarn/* 3 | !.yarn/releases 4 | !.yarn/plugins 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | node_modules/ 9 | postgres_data/ 10 | .next 11 | packages/api/db.sqlite 12 | packages/api/src/ormconfig.ts 13 | packages/web/db.sqlite 14 | packages/web/server/index.js 15 | # macOS 16 | .DS_Store 17 | 18 | # sql/redis db 19 | *.sqlite 20 | *.rdb 21 | 22 | **/.env 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @heybereket @HenryLeC 2 | /packages/api/ @heybereket @HenryLeC @Angshu31 @BronzW 3 | /packages/desktop/ @heybereket 4 | /packages/cli/ @dulguuncodes 5 | /packages/sdk/ @Angshu31 6 | /assets/ @SamJakob 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Use this template for reporting a bug. 4 | title: 'Bug: [BUG NAME]' 5 | labels: bug, needs triage 6 | --- 7 | 8 | #### Issue description 9 | 10 | #### Steps to reproduce the issue 11 | 12 | 1. 13 | 2. 14 | 3. 15 | 16 | #### What's the expected result? 17 | 18 | #### What's the actual result? 19 | 20 | #### Additional details / screenshot 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Use this template for requesting a new feature. 4 | title: 'Feat: [FEATURE NAME]' 5 | labels: feature request, needs triage 6 | --- 7 | 8 | #### Please Describe The Problem To Be Solved 9 | 10 | (Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.) 11 | 12 | #### (Optional): Suggest A Solution 13 | 14 | (Replace This Text: A concise description of your preferred solution. Things to address include: 15 | 16 | - Details of the technical implementation 17 | - Tradeoffs made in design decisions 18 | - Caveats and considerations for the future 19 | 20 | If there are multiple solutions, please present each one separately. Save comparisons for the very end.) 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translation_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4E3 Translation Request" 3 | about: Use this template to add a new translation request. 4 | title: 'Translation: [LANGUAGE NAME]' 5 | labels: translation 6 | --- 7 | 8 | #### Name of language 9 | 10 | #### ISO 639-1 language code 11 | 12 | _For help finding this code go here https://www.andiamo.co.uk/resources/iso-language-codes/_ 13 | 14 | #### About you 15 | 16 | - Can you help translate this language? 17 | - Are you fluent in english as well as this langauge? 18 | - Do you know people who would use Oasis if it had support for this language 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description:** 2 | 3 | 4 | 5 | **Related Issue (if applicable):** 6 | 7 | 10 | 11 | **Screenshots:** 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | target-branch: dev 8 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | translation: 2 | - packages/web/public/locales/**/* 3 | web: 4 | - packages/web/**/* 5 | api: 6 | - packages/api/**/* 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: dev 5 | paths: 6 | - 'packages/cli/**' 7 | pull_request: 8 | branches: dev 9 | paths: 10 | - 'packages/cli/**' 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: yarn install 20 | - run: yarn build cli 21 | - run: yarn test:ci cli 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: Code Scanning 2 | 3 | on: 4 | push: 5 | branches: dev 6 | pull_request: 7 | branches: dev 8 | schedule: 9 | - cron: '0 0 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: ['typescript'] 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@main 24 | 25 | # Initializes the CodeQL tools for scanning. 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@main 28 | with: 29 | languages: ${{ matrix.language }} 30 | 31 | # Run CodeQL Autobuild 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@main 34 | 35 | # Run CodeQL Analysis 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@main 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yarn v2 2 | .yarn/* 3 | !.yarn/releases 4 | !.yarn/plugins 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | node_modules/ 9 | postgres_data/ 10 | .next 11 | packages/api/src/ormconfig.ts 12 | packages/api/db.sqlite 13 | packages/web/db.sqlite 14 | packages/web/server/index.js 15 | # macOS 16 | .DS_Store 17 | 18 | # sql/redis db 19 | *.sqlite 20 | *.rdb 21 | 22 | .dockerenv 23 | 24 | # vim sessions 25 | Session.vim 26 | 27 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # List the ports you want to expose and what to do when they are served. See https://www.gitpod.io/docs/config-ports/ 2 | ports: 3 | - port: 3000 4 | onOpen: notify 5 | 6 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/config-start-tasks/ 7 | tasks: 8 | - init: yarn && yarn setup:remote # runs during prebuild 9 | command: echo 'Starting web development server (connected to staging API)' && yarn dev 10 | 11 | github: 12 | prebuilds: 13 | # enable for pull requests coming from this repo (defaults to true) 14 | pullRequests: false 15 | # add a check to pull requests (defaults to true) 16 | addCheck: false 17 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) 18 | addComment: false 19 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | generated 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "semi": true, 4 | "singleQuote": true, 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 7 | spec: "@yarnpkg/plugin-typescript" 8 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 9 | spec: "@yarnpkg/plugin-workspace-tools" 10 | 11 | yarnPath: .yarn/releases/yarn-2.4.2.cjs 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Oasis 2 | Interested in contributing to Oasis? Check out our [documentation](/docs) on how to get started. 3 | -------------------------------------------------------------------------------- /assets/logos/png/oasis-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/assets/logos/png/oasis-logo-white.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose to use in dev 2 | 3 | version: "3.9" # optional since v1.27.0 4 | services: 5 | web: 6 | build: . 7 | ports: 8 | - "3000:3000" 9 | links: 10 | - redis 11 | - postgres 12 | # To setup auth uncomment the next line and comment out he line after that. 13 | # Then copy the docker/.dcokerenv.example to docker/.dockerenv and add you OAuth keys 14 | 15 | # env_file: docker/.dockerenv 16 | env_file: docker/.dockerenv.example 17 | 18 | redis: 19 | image: redis 20 | 21 | postgres: 22 | image: postgres 23 | environment: 24 | POSTGRES_USER: postgres 25 | POSTGRES_PASSWORD: password 26 | volumes: 27 | - ./postgres-data:/var/lib/postgresql/data 28 | -------------------------------------------------------------------------------- /docker/docker-ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from 'typeorm'; 2 | import { joinRoot } from './utils/common/rootPath'; 3 | 4 | export const ormconfig: ConnectionOptions = { 5 | type: process.env.DATABASE_TYPE as any, 6 | host: process.env.DATABASE_HOST, 7 | username: process.env.DATABASE_USERNAME, 8 | password: process.env.DATABASE_PASSWORD, 9 | uuidExtension: 'uuid-ossp', 10 | entities: [joinRoot('./entities/*.*')], 11 | migrations: [joinRoot('./migrations/*.*')], 12 | }; 13 | 14 | export default ormconfig; 15 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | ./src/ormconfig.ts 5 | *.sqlite 6 | images 7 | -------------------------------------------------------------------------------- /packages/api/__tests__/BotAuth.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('Testing Bot Authentication', () => { 5 | let botToken: string; 6 | 7 | it('Create A Bot', async () => { 8 | const userClient = createClient(); 9 | 10 | const mutation = gql` 11 | mutation { 12 | createBot(data: { username: "testingBot", name: "Testing Bot" }) 13 | } 14 | `; 15 | 16 | const res = await userClient.mutate({ 17 | mutation, 18 | }); 19 | 20 | botToken = res.data.createBot; 21 | expect(botToken).toBeDefined(); 22 | }); 23 | 24 | it('Get current user as bot', async () => { 25 | const botClient = createClient('BOT ' + botToken); 26 | 27 | const query = gql` 28 | query { 29 | currentUser { 30 | username 31 | } 32 | } 33 | `; 34 | 35 | const res = await botClient.query({ query }); 36 | 37 | expect(res.data.currentUser).toBeTruthy(); 38 | expect(res.data.currentUser.username).toBe('testingBot'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/api/__tests__/CreatePost.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('Testing CreatePost mutation', () => { 5 | it('Expect valid response if given valid mutation', async () => { 6 | const createPostMutation = gql` 7 | mutation { 8 | createPost(data: { message: "test post", topics: [] }) 9 | } 10 | `; 11 | const res = await createClient().mutate({ mutation: createPostMutation }); 12 | 13 | expect(res.data).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/api/__tests__/CreateResort.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('CreateResort mutation test', () => { 5 | const createResortMutation = gql` 6 | mutation { 7 | createResort( 8 | data: { 9 | name: "Test Resort" 10 | description: "Test Description" 11 | banner: "Test Banner URL" 12 | logo: "Test Logo URL" 13 | category: "test category" 14 | } 15 | ) 16 | } 17 | `; 18 | 19 | it('Expect to createResort mutation to return true when trying to create a valid resort', async () => { 20 | const res = ( 21 | await createClient().mutate({ mutation: createResortMutation }) 22 | ).data.createResort; 23 | 24 | expect(res).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/api/__tests__/CurrentUser.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-core'; 3 | 4 | describe('Tests Current User', () => { 5 | it('Get Current User', async () => { 6 | const query = gql` 7 | query { 8 | currentUser { 9 | id 10 | username 11 | } 12 | } 13 | `; 14 | const res = await createClient().query({ 15 | query, 16 | }); 17 | 18 | expect(res.data.currentUser.username).toBe('testing'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/api/__tests__/GetNotifications.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('GetNotifications query test', () => { 5 | const getNotificationsQuery = gql` 6 | { 7 | getNotifications { 8 | id 9 | } 10 | } 11 | `; 12 | 13 | it('Expect get notifications query to be type of array', async () => { 14 | const res = (await createClient().query({ query: getNotificationsQuery })) 15 | .data.getNotifications; 16 | 17 | expect(res).toBeInstanceOf(Array); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/api/__tests__/GetQueue.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('Testing GetQueue query', () => { 5 | const getQueueQuery = gql` 6 | { 7 | getQueue { 8 | id 9 | } 10 | } 11 | `; 12 | 13 | it('Expect error if user does not have permission', async () => { 14 | expect( 15 | createClient().query({ 16 | query: getQueueQuery, 17 | }) 18 | ).rejects.toBeDefined(); 19 | }); 20 | 21 | it('Expect a valid response if user does have permission', async () => { 22 | const res = await createClient('TESTING adminTesting').query({ 23 | query: getQueueQuery, 24 | }); 25 | expect(res.data.getQueue).toBeInstanceOf(Array); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/api/__tests__/MakeBadge.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('Testing making badges', () => { 5 | const badgeMutation = gql` 6 | mutation { 7 | makeBadge( 8 | data: { 9 | name: "test-badge" 10 | imagePath: "" 11 | level: 1 12 | description: "a testing badge" 13 | } 14 | ) 15 | } 16 | `; 17 | 18 | it('Expect error if making a badge without being an admin', async () => { 19 | expect( 20 | createClient().mutate({ mutation: badgeMutation }) 21 | ).rejects.toBeDefined(); 22 | }); 23 | 24 | it('Expect badge to be made when admin', async () => { 25 | const res = await createClient('TESTING adminTesting').mutate({ 26 | mutation: badgeMutation, 27 | }); 28 | expect(res.data.makeBadge).toBe(true); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/api/__tests__/MakeReport.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { reporteeUserId } from '@utils/testing/seedDatabase'; 3 | import { gql } from 'apollo-server-express'; 4 | 5 | describe('MakeReport mutation test', () => { 6 | it('Expect to createReport mutation to return true when trying to create a valid report', async () => { 7 | const createReportMutation = gql` 8 | mutation CreateReport($id: String!) { 9 | makeReport( 10 | reporteeId: $id 11 | data: { type: InappropriateContent, information: "Test Report" } 12 | ) 13 | } 14 | `; 15 | 16 | const res = ( 17 | await createClient().mutate({ 18 | mutation: createReportMutation, 19 | variables: { id: reporteeUserId }, 20 | }) 21 | ).data.makeReport; 22 | 23 | expect(res).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/api/__tests__/PaginateUsers.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-core'; 3 | 4 | describe('Paginate Users test', () => { 5 | it('Paginate Users', async () => { 6 | const paginateQuery = gql` 7 | query { 8 | paginateUsers(limit: 10, offset: 0) { 9 | id 10 | } 11 | } 12 | `; 13 | const res = await createClient().query({ 14 | query: paginateQuery, 15 | }); 16 | expect(res.data.paginateUsers).toBeInstanceOf(Array); 17 | expect(res.data.paginateUsers.length).toBeGreaterThanOrEqual(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/api/__tests__/Search.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '../src/utils/testing/gql-request'; 2 | import { gql } from 'apollo-server-express'; 3 | 4 | describe('Testing Search Query', () => { 5 | it('Testing if search query works after given valid query', async () => { 6 | const searchQuery = gql` 7 | { 8 | search(limit: 999, searchQuery: "") { 9 | ... on User { 10 | id 11 | name 12 | } 13 | } 14 | } 15 | `; 16 | 17 | const res = await createClient().query({ query: searchQuery }); 18 | const search: any[] = res.data.search; 19 | expect(search).toBeInstanceOf(Array); 20 | expect(search.length).toBeGreaterThan(0); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/api/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | export default async (): Promise => { 4 | return { 5 | verbose: false, 6 | rootDir: './__tests__', 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/api/src/auth/connections/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import Spotify from './methods/spotify'; 3 | 4 | export default (): Router => { 5 | const connectionRouter = Router(); 6 | 7 | // OAuth Providers 8 | if (process.env.SPOTIFY_CLIENT_ID) 9 | connectionRouter.use('/spotify', Spotify()); 10 | 11 | return connectionRouter; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/api/src/build.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { createSchema } from '@utils/files/createSchema'; 3 | import * as log from '@utils/output/log'; 4 | import { exit } from '@utils/output/exit'; 5 | 6 | try { 7 | createSchema(); 8 | log.event('successfully compiled api'); 9 | process.exit(); 10 | } catch (err) { 11 | log.error(`failed to compile api: ${err}`); 12 | exit(1); 13 | } 14 | -------------------------------------------------------------------------------- /packages/api/src/config/database.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions, createConnection } from 'typeorm'; 2 | import { joinRoot } from '@utils/common/rootPath'; 3 | import { seedDatabase } from '@utils/testing/seedDatabase'; 4 | import * as log from '@utils/output/log'; 5 | 6 | export const getDatabase = async () => { 7 | let ormconfig: ConnectionOptions; 8 | 9 | if (process.env.TESTING === 'true') { 10 | seedDatabase(); 11 | ormconfig = { 12 | type: 'sqlite', 13 | database: 'testing.sqlite', 14 | entities: [joinRoot('./entities/*.*')], 15 | synchronize: true, 16 | }; 17 | } else { 18 | ormconfig = require('@root/ormconfig').default; 19 | } 20 | 21 | try { 22 | await createConnection(ormconfig); 23 | log.event(`successfully connected to ${ormconfig.type} database`); 24 | } catch (err) { 25 | log.error(`failed to connect to database: ${err}`); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/api/src/config/redis.ts: -------------------------------------------------------------------------------- 1 | import connectRedis from 'connect-redis'; 2 | import expressSession from 'express-session'; 3 | import Redis from 'ioredis'; 4 | import { redisURL } from '@lib/constants'; 5 | 6 | export const redisStore = connectRedis(expressSession); 7 | export const redisClient = new Redis(redisURL); 8 | -------------------------------------------------------------------------------- /packages/api/src/config/s3.ts: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | 3 | export default () => 4 | new AWS.S3({ 5 | credentials: { 6 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 7 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 8 | }, 9 | endpoint: `https://${process.env.AWS_ENDPOINT}`, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/api/src/config/urls.ts: -------------------------------------------------------------------------------- 1 | export const URLs = { 2 | authSuccess: '/auth/success', 3 | login: '/login', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/api/src/entities/Badge.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from 'type-graphql'; 2 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @ObjectType() 5 | @Entity() 6 | export default class Badge extends BaseEntity { 7 | @PrimaryGeneratedColumn('uuid') 8 | @Field(() => ID) 9 | id: string; 10 | 11 | @Column() 12 | @Field() 13 | name: string; 14 | 15 | @Column() 16 | @Field() 17 | imagePath: string; 18 | 19 | @Column() 20 | @Field() 21 | level: number; 22 | 23 | @Column() 24 | @Field() 25 | description: string; 26 | 27 | @Column() 28 | @Field() 29 | createdAt: string; 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/entities/Connection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | ManyToOne, 6 | Entity, 7 | } from 'typeorm'; 8 | import User from './User'; 9 | 10 | @Entity() 11 | export default class Connection extends BaseEntity { 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string; 14 | 15 | @Column() 16 | accessToken: string; 17 | 18 | @Column() 19 | refreshToken: string; 20 | 21 | @ManyToOne(() => User, (user) => user.connections) 22 | user: Promise; 23 | 24 | @Column() 25 | connectionMethod: String; 26 | } 27 | -------------------------------------------------------------------------------- /packages/api/src/entities/Notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationType } from '@enums/Notifications'; 2 | import { Field, ID, ObjectType } from 'type-graphql'; 3 | import { 4 | BaseEntity, 5 | Column, 6 | Entity, 7 | ManyToOne, 8 | PrimaryGeneratedColumn, 9 | } from 'typeorm'; 10 | import User from './User'; 11 | 12 | @ObjectType() 13 | @Entity() 14 | export default class Notification extends BaseEntity { 15 | @Field() 16 | @Column() 17 | createdAt: string; 18 | 19 | @PrimaryGeneratedColumn('uuid') 20 | @Field(() => ID) 21 | id: string; 22 | 23 | @Field(() => NotificationType) 24 | @Column() 25 | type: NotificationType; 26 | 27 | @Field(() => User) 28 | @ManyToOne(() => User, (user) => user.notifications) 29 | user: Promise; 30 | 31 | @Field(() => User) 32 | @ManyToOne(() => User) 33 | performer: Promise; 34 | 35 | @Field() 36 | @Column() 37 | read: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /packages/api/src/entities/PremiumToken.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from 'type-graphql'; 2 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | @ObjectType() 6 | export class PremiumToken extends BaseEntity { 7 | @PrimaryGeneratedColumn('uuid') 8 | @Field(() => ID) 9 | id: string; 10 | 11 | @Field() 12 | @Column() 13 | premiumTime: number; 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/enums/Notifications.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from 'type-graphql'; 2 | 3 | export enum NotificationType { 4 | Follow = 'FOLLOW', 5 | UpvotePost = 'LIKE_POST', 6 | Comment = 'COMMENT', 7 | UpvoteComment = 'LIKE_COMMENT', 8 | ReplyComment = 'REPLY_COMMENT', 9 | } 10 | 11 | registerEnumType(NotificationType, { 12 | name: 'NotificationType', 13 | }); 14 | -------------------------------------------------------------------------------- /packages/api/src/enums/Reports.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from 'type-graphql'; 2 | 3 | export enum ReportType { 4 | InappropriateContent = 'INAPPROPRIATE_CONTENT', 5 | } 6 | 7 | registerEnumType(ReportType, { 8 | name: 'ReportType', 9 | }); 10 | -------------------------------------------------------------------------------- /packages/api/src/enums/Roles.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from 'type-graphql'; 2 | 3 | export enum Role { 4 | SuperAdmin = 'SUPERADMIN', 5 | Admin = 'ADMIN', 6 | Moderator = 'MODERATOR', 7 | } 8 | 9 | registerEnumType(Role, { 10 | name: 'Role', 11 | }); 12 | -------------------------------------------------------------------------------- /packages/api/src/enums/Status.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from 'type-graphql'; 2 | 3 | export enum Status { 4 | Online = 'Online', 5 | Away = 'Away', 6 | DoNotDisturb = 'DoNotDisturb', 7 | Offline = 'Offline', 8 | } 9 | 10 | registerEnumType(Status, { 11 | name: 'Status', 12 | }); 13 | -------------------------------------------------------------------------------- /packages/api/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | // General 2 | export const nodeMajor = Number(process.versions.node.split('.')[0]); 3 | export const PORT = Number(process.env.PORT) || 3000; 4 | 5 | // Limits 6 | export const complexityLimit = 50; 7 | export const rateLimitTime = 60 * 60; 8 | 9 | // Environment Variables 10 | export const isProduction = process.env.NODE_ENV === 'production'; 11 | export const sessionSecret = process.env.SESSION_SECRET || 'oasis_session'; 12 | export const redisURL = process.env.REDIS_URL || 'redis://localhost:6379'; 13 | 14 | // Regex Patterns 15 | export const usernameRegex = /^[a-zA-Z0-9_.-]{3,15}$/; 16 | -------------------------------------------------------------------------------- /packages/api/src/lib/nodeMajor.ts: -------------------------------------------------------------------------------- 1 | import { nodeMajor } from '@lib/constants'; 2 | import * as log from '@utils/output/log'; 3 | import { exit } from '@utils/output/exit'; 4 | 5 | export const checkNodeMajor = (version: number) => { 6 | if (nodeMajor < version) { 7 | log.error( 8 | `You are currently running on Node ${nodeMajor}. Oasis requires Node ${version} or higher.` 9 | ); 10 | exit(1); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/api/src/middleware/NotBanned.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFn } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import User from '@entities/User'; 4 | import { ApolloError } from 'apollo-server-errors'; 5 | 6 | export function NotBanned(force: boolean): MiddlewareFn { 7 | return async ({ context, info }, next) => { 8 | const user: User = await (context as ContextType).getUser(); 9 | 10 | const operationDescription = 11 | info.parentType.getFields()[info.fieldName].description; 12 | 13 | if ( 14 | (info.operation.operation === 'mutation' && 15 | !(operationDescription ?? '').includes('[NO-BAN]')) || 16 | force 17 | ) { 18 | if (Number.parseInt(user.banExiration ?? '0') > Date.now()) { 19 | throw new ApolloError('Sorry, you are banned', 'BANNED_USER'); 20 | } 21 | } 22 | 23 | return next(); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/api/src/middleware/SelfOnly.ts: -------------------------------------------------------------------------------- 1 | import { ContextType } from '@root/server'; 2 | import { ApolloError } from 'apollo-server-errors'; 3 | import { createMethodDecorator } from 'type-graphql'; 4 | 5 | export function SelfOnly() { 6 | return createMethodDecorator(async ({ root, context }, next) => { 7 | if (root.id !== (await (context as ContextType).getUser()).id) { 8 | throw new ApolloError( 9 | 'You can only get this property on your user', 10 | 'SELF_ONLY' 11 | ); 12 | } 13 | return next(); 14 | }) as PropertyDecorator; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/answer/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Answer from '@entities/Answer'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateAnswers, answer) 5 | // @bcg-resolver(query, getAnswer, answer) 6 | 7 | export default createResolver('Answer', Answer); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/answer/edit/EditAnswerInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class EditAnswerInput { 5 | @Field() 6 | content: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/answer/new/NewAnswerInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class NewAnswerInput { 5 | @Field() 6 | content: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/badge/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Badge from '@entities/Badge'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateBadges, badge) 5 | // @bcg-resolver(query, getBadge, badge) 6 | 7 | export default createResolver('Badge', Badge); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/badge/GiveBadge.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Mutation, Resolver } from 'type-graphql'; 2 | import Badge from '@entities/Badge'; 3 | import { Role } from '@enums/Roles'; 4 | import User from '@entities/User'; 5 | 6 | // @bcg-resolver(mutation, giveBadge, badge) 7 | 8 | @Resolver() 9 | export default class GiveBadgeResolver { 10 | @Mutation(() => Boolean) 11 | @Authorized(Role.Admin) 12 | async giveBadge( 13 | @Arg('username') username: string, 14 | @Arg('badgeName') badgeName: string 15 | ) { 16 | const user = await User.createQueryBuilder() 17 | .where('username = :username', { username }) 18 | .getOne(); 19 | 20 | user.badges = Promise.resolve([ 21 | ...(await user.badges), 22 | await Badge.createQueryBuilder() 23 | .where('name = :name', { 24 | name: badgeName, 25 | }) 26 | .getOne(), 27 | ]); 28 | 29 | await user.save(); 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/badge/make/MakeBadge.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Mutation, Resolver } from 'type-graphql'; 2 | import MakeBadgeInput from './MakeBadgeInput'; 3 | import Badge from '@entities/Badge'; 4 | import { Role } from '@enums/Roles'; 5 | 6 | // @bcg-resolver(mutation, makeBadge, badge) 7 | @Resolver() 8 | export default class MakeBadgeResolver { 9 | @Mutation(() => Boolean) 10 | @Authorized(Role.Admin) 11 | async makeBadge(@Arg('data') data: MakeBadgeInput) { 12 | const badge = Badge.create(); 13 | 14 | Badge.merge(badge, data); 15 | 16 | badge.createdAt = String(Date.now()); 17 | 18 | await badge.save(); 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/badge/make/MakeBadgeInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class MakeBadgeInput { 5 | @Field() 6 | name: string; 7 | 8 | @Field() 9 | imagePath: string; 10 | 11 | @Field() 12 | level: number; 13 | 14 | @Field() 15 | description: string; 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/comment/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Comment from '@entities/Comment'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateComments, comment) 5 | // @bcg-resolver(query, getComment, comment) 6 | 7 | export default createResolver('Comment', Comment); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/comment/edit/EditCommentInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class EditCommentInput { 5 | @Field() 6 | content: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/comment/new/NewCommentInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class NewCommentInput { 5 | @Field() 6 | content: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/notification/GetNotifications.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver, Ctx, Authorized } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import Notification from '@entities/Notification'; 4 | import { NoBot } from '@utils/auth/NoBot'; 5 | 6 | @Resolver() 7 | export default class GetNotifications { 8 | @Authorized() 9 | @NoBot() /* Added as this is a resolver meant for UI purposes */ 10 | @Query(() => [Notification], { nullable: true }) 11 | async getNotifications(@Ctx() { getUser }: ContextType) { 12 | const user = await getUser(); 13 | 14 | return await Notification.find({ where: { user } }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/post/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Post from '@entities/Post'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginatePosts, post) 5 | // @bcg-resolver(query, getPost, post) 6 | 7 | export default createResolver('Post', Post); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/post/edit/EditPostInput.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export default class EditPostInput { 6 | @Field({ nullable: true }) 7 | @Length(1, 1000) 8 | message: string; 9 | 10 | @Field(() => [String], { nullable: true }) 11 | topics: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/post/feed/FeedSort.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Query, Resolver } from 'type-graphql'; 2 | import Post from '@entities/Post'; 3 | 4 | @Resolver() 5 | export default class DeletePostResolver { 6 | @Query(() => [Post]) 7 | async feedSortPosts( 8 | @Arg('limit') limit: number, 9 | @Arg('offset') offset: number 10 | ) { 11 | return await Post.createQueryBuilder('post') 12 | .groupBy('post.id') 13 | .orderBy('post.upvotes - post.downvotes', 'DESC') 14 | .addOrderBy('post.createdAt', 'DESC') 15 | .offset(offset) 16 | .limit(limit) 17 | .getMany(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/post/new/CreatePost.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import Post from '@entities/Post'; 4 | import { customAlphabet } from 'nanoid'; 5 | import CreatePostInput from './CreatePostInput'; 6 | 7 | // @bcg-resolver(mutation, createPost, post) 8 | 9 | @Resolver() 10 | export default class CreatePostResolver { 11 | @Mutation(() => Boolean) 12 | @Authorized() 13 | async createPost( 14 | @Arg('data') data: CreatePostInput, 15 | @Ctx() { getUser }: ContextType 16 | ) { 17 | const newPost = Post.create(); 18 | const nanoid = customAlphabet('1234567890abcdef', 10); 19 | Post.merge(newPost, data); 20 | 21 | newPost.createdAt = String(Date.now()); 22 | 23 | newPost.author = Promise.resolve(await getUser()); 24 | newPost.createdAt = String(Date.now()); 25 | newPost.id = nanoid(); 26 | 27 | newPost.save(); 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/post/new/CreatePostInput.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export default class CreatePostInput { 6 | @Field({ nullable: false }) 7 | @Length(1, 1000) 8 | message: string; 9 | 10 | @Field(() => [String], { nullable: false }) 11 | topics: string[]; 12 | 13 | @Field({ nullable: true }) 14 | imageName?: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/question/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Question from '@entities/Question'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateQuestions, question) 5 | // @bcg-resolver(query, getQuestion, question) 6 | 7 | export default createResolver('Question', Question); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/question/edit/EditQuestionInput.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export default class EditQuestionInput { 6 | @Field({ nullable: true }) 7 | @Length(1, 1000) 8 | message: string; 9 | 10 | @Field(() => [String], { nullable: true }) 11 | topics: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/question/new/CreateQuestion.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import Question from '@entities/Question'; 4 | import { customAlphabet } from 'nanoid'; 5 | import CreateQuestionInput from './CreateQuestionInput'; 6 | 7 | // @bcg-resolver(mutation, createQuestion, question) 8 | 9 | @Resolver() 10 | export default class CreateQuestionResolver { 11 | @Mutation(() => Boolean) 12 | @Authorized() 13 | async createQuestion( 14 | @Arg('data') data: CreateQuestionInput, 15 | @Ctx() { getUser }: ContextType 16 | ) { 17 | const newQuestion = Question.create(); 18 | const nanoid = customAlphabet('1234567890abcdef', 10); 19 | Question.merge(newQuestion, data); 20 | 21 | newQuestion.createdAt = String(Date.now()); 22 | 23 | newQuestion.author = Promise.resolve(await getUser()); 24 | newQuestion.createdAt = String(Date.now()); 25 | newQuestion.id = nanoid(); 26 | 27 | newQuestion.save(); 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/question/new/CreateQuestionInput.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export default class CreateQuestionInput { 6 | @Field({ nullable: false }) 7 | @Length(1, 1000) 8 | message: string; 9 | 10 | @Field(() => [String], { nullable: false }) 11 | topics: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/reports/GetQueue.resolver.ts: -------------------------------------------------------------------------------- 1 | import Report from '@entities/Report'; 2 | import { Role } from '@enums/Roles'; 3 | import { Authorized, Query, Resolver } from 'type-graphql'; 4 | 5 | @Resolver() 6 | export default class GetQueueResolver { 7 | @Authorized(Role.Admin) 8 | @Query(() => [Report]) 9 | async getQueue() { 10 | return await Report.find({ 11 | where: { resolved: false }, 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/reports/MakeReportInput.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsNotEmpty, Length } from 'class-validator'; 2 | import { InputType, Field } from 'type-graphql'; 3 | import { ReportType } from '@enums/Reports'; 4 | 5 | @InputType() 6 | export default class MakeReportInput { 7 | @Field(() => ReportType) 8 | type: ReportType; 9 | 10 | @Field({ nullable: true }) 11 | @IsOptional() 12 | @IsNotEmpty() 13 | @Length(0, 400) 14 | information?: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/reports/MarkAsResolved.resolver.ts: -------------------------------------------------------------------------------- 1 | import Report from '@entities/Report'; 2 | import { Role } from '@enums/Roles'; 3 | import { Arg, Authorized, Mutation, Resolver } from 'type-graphql'; 4 | 5 | @Resolver() 6 | export default class MarkAsResolvedResolver { 7 | @Authorized(Role.Moderator) 8 | @Mutation(() => Boolean) 9 | async markAsResolved( 10 | @Arg('reportId') reportId: string, 11 | @Arg('resolved') resolved: boolean 12 | ) { 13 | const report = await Report.findOne(reportId); 14 | report.resolved = resolved; 15 | report.save(); 16 | 17 | return true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/reports/ReportedEntityInput.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class ReportedEntityInput { 5 | @Field(() => String, { nullable: true }) 6 | userId?: string; 7 | 8 | @Field(() => String, { nullable: true }) 9 | postId?: string; 10 | 11 | @Field(() => String, { nullable: true }) 12 | commentId?: string; 13 | 14 | @Field(() => String, { nullable: true }) 15 | resortId?: string; 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/resort/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import Resort from '@entities/Resort'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateResorts, resort) 5 | // @bcg-resolver(query, getResort, resort) 6 | 7 | export default createResolver('Resort', Resort); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/resort/GetIsJoined.resolver.ts: -------------------------------------------------------------------------------- 1 | import Resort from '@entities/Resort'; 2 | import User from '@entities/User'; 3 | import { ContextType } from '@root/server'; 4 | import { 5 | Arg, 6 | Authorized, 7 | Ctx, 8 | FieldResolver, 9 | Query, 10 | Resolver, 11 | Root, 12 | } from 'type-graphql'; 13 | 14 | @Resolver(() => Resort) 15 | export default class PaginateResortMembersResolver { 16 | @FieldResolver(() => Boolean) 17 | async isJoined( 18 | @Root() resort: Resort, 19 | @Ctx() { getUser, hasAuth }: ContextType 20 | ) { 21 | if (hasAuth) { 22 | const user = await getUser(); 23 | let retValue = false; 24 | (await user.joinedResorts).forEach((res) => { 25 | if (res.id === resort.id) { 26 | retValue = true; 27 | } 28 | }); 29 | return retValue; 30 | } else { 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/resort/GetResortByName.resolver.ts: -------------------------------------------------------------------------------- 1 | import Resort from '@entities/Resort'; 2 | import { Arg, Query, Resolver } from 'type-graphql'; 3 | 4 | // @bcg-resolver(query, getResortByName, resort) 5 | 6 | @Resolver() 7 | export default class GetResortByNameResolver { 8 | @Query(() => Resort, { nullable: true }) 9 | getResortByName(@Arg('name') name: string) { 10 | return Resort.createQueryBuilder('resort') 11 | .where('LOWER(resort.name) = LOWER(:name)', { name }) 12 | .getOne(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/resort/create/CreateResort.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import CreateResortInput from './CreateResortInput'; 4 | import Resort from '@entities/Resort'; 5 | 6 | @Resolver() 7 | export default class CreateResortResolver { 8 | @Mutation(() => Boolean) 9 | @Authorized() 10 | async createResort( 11 | @Arg('data') data: CreateResortInput, 12 | @Ctx() { getUser }: ContextType 13 | ) { 14 | const newResort = Resort.create(); 15 | Resort.merge(newResort, data); 16 | 17 | newResort.createdAt = String(Date.now()); 18 | 19 | newResort.owner = Promise.resolve(await getUser()); 20 | newResort.save(); 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/resort/create/CreateResortInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export default class CreateResortInput { 5 | @Field({ nullable: false }) 6 | name: string; 7 | 8 | @Field({ nullable: false }) 9 | description: string; 10 | 11 | @Field({ nullable: false }) 12 | banner: string; 13 | 14 | @Field({ nullable: false }) 15 | logo: string; 16 | 17 | @Field({ nullable: false }) 18 | category: string; 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/BanUser.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import User from '@entities/User'; 4 | import { Role } from '@enums/Roles'; 5 | import { ApolloError } from 'apollo-server-errors'; 6 | 7 | @Resolver() 8 | export default class BanUser { 9 | @Mutation(() => Boolean) 10 | @Authorized(Role.Moderator) 11 | async banUser( 12 | @Arg('UserId') userId: string, 13 | @Arg('endDate') endDate: string 14 | ) { 15 | const user = await User.findOne(userId); 16 | 17 | if (!user) { 18 | throw new ApolloError('User not found'); 19 | } 20 | 21 | user.banExiration = endDate; 22 | 23 | user.save(); 24 | 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/Base.resolver.ts: -------------------------------------------------------------------------------- 1 | import User from '@entities/User'; 2 | import { createResolver } from '@utils/files/createResolver'; 3 | 4 | // @bcg-resolver(query, paginateUsers, user) 5 | // @bcg-resolver(query, getUser, user) 6 | 7 | export default createResolver('User', User); 8 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/CurrentUser.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Authorized, Ctx, Query, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import User from '@entities/User'; 4 | 5 | // @bcg-resolver(query, currentUser, user) 6 | 7 | @Resolver() 8 | export default class CurrentUser { 9 | @Query(() => User, { nullable: true }) 10 | @Authorized() 11 | currentUser(@Ctx() { getUser }: ContextType) { 12 | return getUser(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/DeleteAccount.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Authorized, Ctx, Mutation, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import { NoBot } from '@utils/auth/NoBot'; 4 | 5 | @Resolver() 6 | export default class DeleteAccountResolver { 7 | @Mutation(() => Boolean) 8 | @Authorized() 9 | @NoBot() 10 | async deleteAccount(@Ctx() { getUser }: ContextType) { 11 | const user = await getUser(); 12 | await user.remove(); 13 | 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/GetUserByName.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Arg, Resolver } from 'type-graphql'; 2 | import User from '@entities/User'; 3 | 4 | // @bcg-resolver(query, getUserByName, user) 5 | 6 | @Resolver() 7 | export default class GetUserByNameResolver { 8 | @Query(() => User, { nullable: true }) 9 | async getUserByName(@Arg('username') username: string) { 10 | return await User.createQueryBuilder('user') 11 | .where('LOWER(user.username) = LOWER(:username)', { username }) 12 | .getOne(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/JoinResort.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Resolver, Authorized, Ctx, Mutation } from 'type-graphql'; 2 | import Resort from '@entities/Resort'; 3 | import { ApolloError } from 'apollo-server-errors'; 4 | import { ContextType } from '@root/server'; 5 | 6 | // @bcg-resolver(mutation, joinResort, resort) 7 | 8 | @Resolver() 9 | export default class JoinResortResolver { 10 | @Mutation(() => Boolean, { nullable: true }) 11 | @Authorized() 12 | async joinResort( 13 | @Arg('resortId') resortId: string, 14 | @Ctx() { getUser }: ContextType 15 | ) { 16 | const resort = await Resort.findOne(resortId); 17 | if (!resort) throw new ApolloError('Resort not found', 'RESORT_NOT_FOUND'); 18 | const user = await getUser(); 19 | 20 | user.joinedResorts = Promise.resolve([ 21 | ...(await user.joinedResorts), 22 | resort, 23 | ]); 24 | 25 | user.save(); 26 | 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/MakeAdmin.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Mutation, Resolver } from 'type-graphql'; 2 | import User from '@entities/User'; 3 | import { Role } from '@enums/Roles'; 4 | 5 | @Resolver() 6 | export default class MakeAdminResolver { 7 | @Mutation(() => Boolean) 8 | @Authorized(Role.SuperAdmin) 9 | async makeAdmin( 10 | @Arg('roles', () => [Role]) roles: Role[], 11 | @Arg('user') username: string 12 | ) { 13 | const user = await User.createQueryBuilder('user') 14 | .where('LOWER(user.username) = LOWER(:username)', { username }) 15 | .getOne(); 16 | 17 | user.roles = roles; 18 | 19 | user.save(); 20 | 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/premium/GetTokenData.resolver.ts: -------------------------------------------------------------------------------- 1 | import { PremiumToken } from '@entities/PremiumToken'; 2 | import { Arg, Mutation, Resolver } from 'type-graphql'; 3 | 4 | @Resolver() 5 | export default class TokenData { 6 | @Mutation(() => PremiumToken) 7 | async getTokenData(@Arg('tokenId') tokenId: string) { 8 | return PremiumToken.findOne(tokenId); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/premium/MakePremiumToken.resolver.ts: -------------------------------------------------------------------------------- 1 | import { PremiumToken } from '@entities/PremiumToken'; 2 | import { Arg, Mutation, Resolver } from 'type-graphql'; 3 | 4 | @Resolver() 5 | export default class MakePremiumToken { 6 | @Mutation(() => String) 7 | async makePremiumToken(@Arg('time') time: number) { 8 | const token = PremiumToken.create({ 9 | premiumTime: time, 10 | }); 11 | 12 | await token.save(); 13 | 14 | return token.id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/premium/RedeemToken.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import User from '@entities/User'; 4 | import { Role } from '@enums/Roles'; 5 | import { ApolloError } from 'apollo-server-errors'; 6 | import { verify } from 'jsonwebtoken'; 7 | import { PremiumToken } from '@entities/PremiumToken'; 8 | 9 | @Resolver() 10 | export default class RedeemToken { 11 | @Mutation(() => Boolean) 12 | @Authorized() 13 | async redeemPremium( 14 | @Arg('tokenId') tokenId: string, 15 | @Ctx() { getUser }: ContextType 16 | ) { 17 | const user = await getUser(); 18 | const token = await PremiumToken.findOne(tokenId); 19 | 20 | user.premiumExiration = ( 21 | (!Number.isNaN(Number.parseInt(user.premiumExiration)) 22 | ? Number.parseInt(user.premiumExiration) 23 | : Date.now()) + token.premiumTime 24 | ).toString(); 25 | 26 | user.save(); 27 | 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/service-auth/CreateTokens.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ContextType } from '@root/server'; 2 | import { sign } from 'jsonwebtoken'; 3 | import { Authorized, Mutation, Resolver, Ctx } from 'type-graphql'; 4 | import { TokenData } from './TokenDataInput'; 5 | 6 | @Resolver() 7 | export default class CreateTokensResolvers { 8 | @Mutation(() => TokenData) 9 | @Authorized() 10 | createTokens(@Ctx() { uid }: ContextType): TokenData { 11 | return { 12 | accessToken: sign( 13 | { uid, count: 0 }, 14 | process.env.VSCODE_ACCESS_TOKEN_SECRET, 15 | { expiresIn: '15m' } 16 | ), 17 | refreshToken: sign({ uid }, process.env.VSCODE_REFRESH_TOKEN_SECRET, { 18 | expiresIn: '30d', 19 | }), 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/service-auth/RefreshToken.resolver.ts: -------------------------------------------------------------------------------- 1 | import User from '@entities/User'; 2 | import { ApolloError } from 'apollo-server-express'; 3 | import { sign, verify } from 'jsonwebtoken'; 4 | import { Mutation, Resolver, Arg } from 'type-graphql'; 5 | 6 | @Resolver() 7 | export default class RefreshTokenResolvers { 8 | @Mutation(() => String) 9 | async refreshToken( 10 | @Arg('refreshToken') refreshToken: string 11 | ): Promise { 12 | try { 13 | const data = verify( 14 | refreshToken, 15 | process.env.VSCODE_REFRESH_TOKEN_SECRET 16 | ) as any; 17 | 18 | const { uid } = data; 19 | const user = await User.findOne(uid); 20 | 21 | const newAccessToken = sign( 22 | { uid, count: ++user.vscTokenCount }, 23 | process.env.VSCODE_ACCESS_TOKEN_SECRET, 24 | { expiresIn: '15m' } 25 | ); 26 | 27 | await user.save(); 28 | return newAccessToken; 29 | } catch (e) { 30 | throw new ApolloError('Invalid Refresh Token!'); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/service-auth/TokenDataInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class TokenData { 5 | @Field() 6 | accessToken: string; 7 | 8 | @Field() 9 | refreshToken: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/api/src/resolvers/user/update/UpdateProfileInput.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsUrl, Length, IsNotEmpty } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export default class UpdateProfileInput { 6 | @Field({ nullable: true }) 7 | @IsOptional() 8 | @IsUrl() 9 | banner?: string; 10 | 11 | @Field({ nullable: true }) 12 | @IsOptional() 13 | @IsUrl() 14 | avatar?: string; 15 | 16 | @Field({ nullable: true }) 17 | @IsOptional() 18 | @IsNotEmpty() 19 | @Length(0, 50) 20 | name?: string; 21 | 22 | @Field({ nullable: true }) 23 | @IsOptional() 24 | @IsNotEmpty() 25 | @Length(0, 20) 26 | username?: string; 27 | 28 | @Field({ nullable: true }) 29 | @IsOptional() 30 | @Length(0, 255) 31 | bio?: string; 32 | } 33 | -------------------------------------------------------------------------------- /packages/api/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { Upload } from './upload'; 3 | 4 | export default (): Router => { 5 | const apiRouter = Router(); 6 | 7 | apiRouter.use('/upload', Upload()); 8 | 9 | return apiRouter; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/api/src/utils/auth/NoBot.ts: -------------------------------------------------------------------------------- 1 | import { ContextType } from '@root/server'; 2 | import { ApolloError } from 'apollo-server-errors'; 3 | import { createMethodDecorator } from 'type-graphql'; 4 | 5 | export const NoBot = () => 6 | createMethodDecorator(async ({ context }, next) => { 7 | const user = await context.getUser(); 8 | 9 | if (user !== undefined) { 10 | if (user.isBot) { 11 | throw new ApolloError('This operation cannot be performed by a bot'); 12 | } 13 | } 14 | 15 | return next(); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/api/src/utils/auth/authChecker.ts: -------------------------------------------------------------------------------- 1 | import { AuthChecker } from 'type-graphql'; 2 | import { ContextType } from '@root/server'; 3 | import { Role } from '@enums/Roles'; 4 | 5 | export const customAuthChecker: AuthChecker = async ( 6 | { context }, 7 | roles 8 | ) => { 9 | if (context.hasAuth) { 10 | const user = await context.getUser(); 11 | 12 | for (const role of roles) { 13 | if (!user.roles.includes(role as Role)) return false; 14 | } 15 | 16 | return true; 17 | } 18 | 19 | return false; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/api/src/utils/auth/checkUsername.ts: -------------------------------------------------------------------------------- 1 | import User from '@entities/User'; 2 | import { getRepository } from 'typeorm'; 3 | import { generatedNumber } from '@utils/index'; 4 | import { usernameRegex } from '@lib/constants'; 5 | import { ApolloError } from 'apollo-server-express'; 6 | 7 | export const checkUsername = async (username: string): Promise => { 8 | const existingWithUsername = await getRepository(User) 9 | .createQueryBuilder('users') 10 | .where('LOWER(username) = LOWER(:username)', { username }) 11 | .getCount(); 12 | 13 | if (existingWithUsername !== 0) { 14 | return username + generatedNumber(4); 15 | } else if (!usernameRegex.test(username)) { 16 | throw new ApolloError('Invalid Username.'); 17 | } else { 18 | return username; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/api/src/utils/common/hasPermission.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@enums/Roles'; 2 | 3 | const order = [Role.SuperAdmin, Role.Admin, Role.Moderator]; 4 | 5 | export const hasPermission = (roles: Role[], targetRole: Role) => { 6 | const targetOrder = order.indexOf(targetRole); 7 | 8 | for (const role of roles) { 9 | const level = order.indexOf(role); 10 | 11 | if (level >= targetOrder) return true; 12 | } 13 | 14 | return false; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/api/src/utils/common/http.ts: -------------------------------------------------------------------------------- 1 | import fetch, { RequestInfo, RequestInit } from 'node-fetch'; 2 | 3 | export const http = (url: RequestInfo, options?: RequestInit) => 4 | fetch(url, options).then((res) => res.json()); 5 | -------------------------------------------------------------------------------- /packages/api/src/utils/common/isNull.ts: -------------------------------------------------------------------------------- 1 | const isNull = (item: string) => { 2 | return item ? item : null; 3 | }; 4 | 5 | export default isNull; 6 | -------------------------------------------------------------------------------- /packages/api/src/utils/common/rootPath.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const rootPath = join(__dirname, '../../../dist'); 4 | export const joinRoot = (...paths: string[]) => join(rootPath, ...paths); 5 | -------------------------------------------------------------------------------- /packages/api/src/utils/connection/createConnection.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@utils/auth/createContext'; 2 | import Connection from '@entities/Connection'; 3 | import { Request } from 'express'; 4 | import * as log from '@utils/output/log'; 5 | 6 | const createConnection = async ( 7 | accessToken: string, 8 | refreshToken: string, 9 | request: Request, 10 | method: string 11 | ) => { 12 | try { 13 | const userPromise = Promise.resolve( 14 | (await createContext(request)).getUser() 15 | ); 16 | const connection = await Connection.create({ 17 | accessToken, 18 | refreshToken, 19 | connectionMethod: method, 20 | user: userPromise, 21 | }).save(); 22 | const user = await userPromise; 23 | user.connections = Promise.resolve([connection]); 24 | console.log(`Success! User connection to ${method} is done`); 25 | } catch (err) { 26 | log.error(err); 27 | } 28 | }; 29 | export default createConnection; 30 | -------------------------------------------------------------------------------- /packages/api/src/utils/files/createSchema.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'type-graphql'; 2 | import { joinRoot } from '@utils/common/rootPath'; 3 | import { customAuthChecker } from '@utils/auth/authChecker'; 4 | import { glob as _glob } from 'glob'; 5 | import { promisify } from 'util'; 6 | import { NotBanned } from '@root/middleware/NotBanned'; 7 | 8 | const glob = promisify(_glob); 9 | 10 | export const createSchema = async () => { 11 | const filenames = await glob(joinRoot('./resolvers/**/*.resolver.js')); 12 | 13 | filenames.push(joinRoot('./utils/paginate/RelationalPaginationResolvers.js')); 14 | 15 | const resolvers: any = filenames 16 | .map((filename) => Object.values(require(filename))) 17 | .flat(); 18 | 19 | return buildSchema({ 20 | resolvers, 21 | emitSchemaFile: joinRoot('../schema.gql'), 22 | authChecker: customAuthChecker, 23 | globalMiddlewares: [NotBanned(false)], 24 | }); 25 | }; 26 | 27 | if (require.main === module) { 28 | process.env.BOT_CLIENT_MODE = 'true'; 29 | } 30 | -------------------------------------------------------------------------------- /packages/api/src/utils/output/exit.ts: -------------------------------------------------------------------------------- 1 | import * as log from './log'; 2 | 3 | export const exit = (amount: number): any => { 4 | log.error( 5 | `${ 6 | amount >= 1 7 | ? `Exiting with ${amount} error` 8 | : `Exiting with ${amount} errors` 9 | }...` 10 | ); 11 | return process.exit(1); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/api/src/utils/output/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const colors = { 4 | error: chalk.red('error') + ' -', 5 | ready: chalk.green('ready') + ' -', 6 | warn: chalk.yellow('warn') + ' -', 7 | event: chalk.magenta('event') + ' -', 8 | info: chalk.magenta('info') + ' -', 9 | }; 10 | 11 | // Ready, no issues 12 | export const ready = (...message: string[]) => { 13 | console.log(colors.ready, ...message); 14 | }; 15 | 16 | // Uh oh, there were issues found 17 | export const error = (...message: string[]) => { 18 | console.error(colors.error, ...message); 19 | }; 20 | 21 | export const warn = (...message: string[]) => { 22 | console.warn(colors.warn, ...message); 23 | }; 24 | 25 | export const event = (...message: string[]) => { 26 | console.log(colors.event, ...message); 27 | }; 28 | 29 | // Information 30 | export const info = (...message: string[]) => { 31 | console.log(colors.info, ...message); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/src/utils/paginate/PaginationResponse.ts: -------------------------------------------------------------------------------- 1 | import { ClassType, Field, ObjectType } from 'type-graphql'; 2 | 3 | export function PaginatedResponse( 4 | TItemClass: ClassType, 5 | TOwner: ClassType, 6 | fieldName: string 7 | ) { 8 | // `isAbstract` decorator option is mandatory to prevent registering in schema 9 | @ObjectType( 10 | `Paginated${TItemClass.name}From${TOwner.name}_${fieldName}Response`, 11 | { 12 | isAbstract: true, 13 | } 14 | ) 15 | abstract class PaginatedResponseClass { 16 | // here we use the runtime argument 17 | @Field(() => [TItemClass]) 18 | // and here the generic type 19 | items: TItem[]; 20 | 21 | @Field() 22 | total: number; 23 | 24 | @Field() 25 | hasMore: boolean; 26 | } 27 | return PaginatedResponseClass as any; 28 | } 29 | -------------------------------------------------------------------------------- /packages/api/src/utils/paginate/RelationalPagination.ts: -------------------------------------------------------------------------------- 1 | import { AdvancedOptions } from 'type-graphql/dist/decorators/types'; 2 | import { BaseEntity } from 'typeorm'; 3 | 4 | type EntityReturner = () => typeof BaseEntity; 5 | 6 | export const mapping = new Map< 7 | EntityReturner, 8 | { 9 | [key: string]: { 10 | getValEntity: EntityReturner; 11 | otherSideKey: string; 12 | options: AdvancedOptions; 13 | }; 14 | } 15 | >(); 16 | 17 | export const RelationalPagination = 18 | ( 19 | getKeyEntity: () => typeof BaseEntity, 20 | getValEntity: () => typeof BaseEntity, 21 | otherSideKey: string, 22 | options: AdvancedOptions = {} 23 | ): PropertyDecorator => 24 | (_, pk) => { 25 | const propKey = String(pk); 26 | 27 | if (!mapping.has(getKeyEntity)) mapping.set(getKeyEntity, {}); 28 | 29 | const allRelations = mapping.get(getKeyEntity); 30 | 31 | allRelations[propKey] = { getValEntity, otherSideKey, options }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/src/utils/testing/gql-request.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from 'cross-fetch'; 2 | import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; 3 | import { setContext } from '@apollo/client/link/context'; 4 | 5 | export const createClient = (authHeader = 'TESTING testing') => { 6 | const httpLink = createHttpLink({ 7 | uri: 'http://localhost:3000/graphql', 8 | fetch, 9 | }); 10 | 11 | const authLink = setContext((_, { headers }) => { 12 | return { 13 | headers: { 14 | ...headers, 15 | authorization: authHeader, 16 | }, 17 | }; 18 | }); 19 | 20 | return new ApolloClient({ 21 | link: authLink.concat(httpLink), 22 | cache: new InMemoryCache(), 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@entities/*": ["src/entities/*"], 6 | "@lib/*": ["src/lib/*"], 7 | "@middleware/*": ["src/middleware/*"], 8 | "@auth/*": ["src/auth/*"], 9 | "@config/*": ["src/config/*"], 10 | "@enums/*": ["src/enums/*"], 11 | "@utils/*": ["src/utils/*"], 12 | "@root/*": ["src/*"] 13 | }, 14 | "rootDir": "src", 15 | "outDir": "dist", 16 | "target": "es2020", 17 | "module": "commonjs", 18 | "lib": ["ESNext"], 19 | "esModuleInterop": true, 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | "skipLibCheck": true, 23 | "importHelpers": true 24 | }, 25 | "exclude": ["jest.config.ts", "__tests__/**/*", "dist/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # npm builds 4 | 5 | *.tgz 6 | -------------------------------------------------------------------------------- /packages/cli/.npmignore: -------------------------------------------------------------------------------- 1 | __tests__/* 2 | examples/* 3 | dist/* 4 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # cli - official oasis command line interface 2 | 3 | ## Installation 4 | 5 | `npm i -g @oasis-sh/cli` 6 | 7 | `yarn global add @oasis-sh/cli` 8 | 9 | NOTE: You may have to prefix sudo in order for it to install properly 10 | 11 | ## building 12 | 13 | `yarn build` 14 | 15 | ## running tests 16 | 17 | `yarn test` 18 | 19 | `yarn test:ci` 20 | 21 | `yarn test:watch` 22 | -------------------------------------------------------------------------------- /packages/cli/__tests__/followUser.test.ts: -------------------------------------------------------------------------------- 1 | describe('following a user', () => { 2 | it.todo('applies changes'); 3 | it.todo('rejects non existant user'); 4 | it.todo('rejects unauthorized requests'); 5 | it.todo('dumps raw json'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/cli/__tests__/getCurrentUser.test.ts: -------------------------------------------------------------------------------- 1 | describe('fetching current user', () => { 2 | it.todo('rejects unauthorized requests'); 3 | it.todo('fetches the correct data'); 4 | it.todo('dumps the raw json'); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/cli/__tests__/getMyUserId.test.ts: -------------------------------------------------------------------------------- 1 | describe('getting my user id', () => { 2 | it.todo('fetches it correctly'); 3 | it.todo('rejects unauthorized requests'); 4 | it.todo('dumps the raw json'); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/cli/__tests__/getPostById.test.ts: -------------------------------------------------------------------------------- 1 | import { execCommand } from './helper'; 2 | import { post as postSchema } from './schemas/postSchema'; 3 | import { matchers } from 'jest-json-schema'; 4 | 5 | expect.extend(matchers); 6 | 7 | describe("retrieving a post by it's id", () => { 8 | const [output, error] = execCommand('get_post_by_id', [ 9 | 'f04f96755e', 10 | '--json', 11 | ]); 12 | 13 | expect(error).toBeNull(); 14 | 15 | const data = JSON.parse(output); 16 | 17 | it('fetches expected data', () => { 18 | expect(data.id).toEqual('f04f96755e'); 19 | }); 20 | 21 | it('yields valid data', () => { 22 | expect(data).toMatchSchema(postSchema); 23 | }); 24 | 25 | it('rejects incomplete requests', () => { 26 | const [_, error] = execCommand('get_post_by_id', ['--json']); 27 | 28 | expect(error).not.toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/cli/__tests__/getUserByName.test.ts: -------------------------------------------------------------------------------- 1 | import { execCommand } from './helper'; 2 | import { user as userSchema } from './schemas/userSchema'; 3 | import { matchers } from 'jest-json-schema'; 4 | 5 | expect.extend(matchers); 6 | 7 | describe('getting user by name', () => { 8 | const [output, error] = execCommand('get_user_by_name', ['bereket', '--json']); 9 | 10 | expect(error).toBeNull(); 11 | expect(output).not.toBeNull(); 12 | 13 | const data = JSON.parse(output); 14 | 15 | it('retrieves valid data', () => { 16 | expect(data).toMatchSchema(userSchema); 17 | }); 18 | 19 | it('fetches the correct user', () => { 20 | const parsed = JSON.parse(output); 21 | 22 | expect(parsed.name).toEqual('Bereket Semagn'); 23 | expect(parsed.username).toEqual('bereket'); 24 | }); 25 | 26 | it('rejects incomplete requests', () => { 27 | const [_, error] = execCommand('get_user_by_name', ['--json']); 28 | 29 | expect(error).not.toBeNull(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/cli/__tests__/getUsersUpvotedPosts.test.ts: -------------------------------------------------------------------------------- 1 | import { post as postSchema } from './schemas/postSchema'; 2 | import { execCommand } from './helper'; 3 | import { matchers } from 'jest-json-schema'; 4 | 5 | expect.extend(matchers); 6 | 7 | describe("getting a user's upvoted posts", () => { 8 | const [output, error] = execCommand('get_users_upvoted_posts', [ 9 | 'bereket', 10 | '--json', 11 | ]); 12 | 13 | expect(error).toBeNull(); 14 | 15 | const data = JSON.parse(output); 16 | 17 | it('yields valid data', () => { 18 | data.items.forEach((post: any) => { 19 | expect(post).toMatchSchema(postSchema); 20 | }); 21 | }); 22 | 23 | it.todo('rejects incomplete requests'); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/cli/__tests__/helper.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | import { gqlURL } from '../src/lib/constants'; 3 | import path from 'path'; 4 | 5 | const serverURL = 6 | process.env.TESTING === 'true' ? 'http://localhost:3000' : gqlURL; 7 | 8 | const execCommand = (command: string, args?: string[]) => { 9 | const { output } = spawnSync('node', [ 10 | path.join(__dirname, '../dist', './bin/oasis.js'), 11 | command, 12 | ...args, 13 | ]); 14 | 15 | const [_, stdout, stderr] = output; 16 | 17 | if (stderr.toString()) return [null, stderr.toString()]; 18 | 19 | return [stdout.toString(), null]; 20 | }; 21 | 22 | export { execCommand, serverURL }; 23 | -------------------------------------------------------------------------------- /packages/cli/__tests__/post.test.ts: -------------------------------------------------------------------------------- 1 | describe('creating a post', () => { 2 | it.todo('applies changes'); 3 | it.todo('validates input'); 4 | it.todo('rejects unauthorized requests'); 5 | it.todo('dumps the raw json'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/cli/__tests__/schemas/postSchema.ts: -------------------------------------------------------------------------------- 1 | export const post = { 2 | $id: 'post', 3 | $schema: 'http://json-schema.org/draft-07/schema#', 4 | description: 'represents an oasis post, used for validating output', 5 | properties: { 6 | message: { type: 'string' }, 7 | author: { 8 | type: 'object', 9 | properties: { 10 | id: { type: 'string' }, 11 | name: { type: 'string' }, 12 | username: { type: 'string' }, 13 | }, 14 | required: ['id', 'name', 'username'], 15 | }, 16 | downvotes: { type: 'number' }, 17 | upvotes: { type: 'number' }, 18 | }, 19 | required: ['message', 'author', 'downvotes', 'upvotes'], 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/__tests__/schemas/querySchema.ts: -------------------------------------------------------------------------------- 1 | export const query = { 2 | $id: 'query', 3 | $schema: 'http://json-schema.org/draft-07/schema#', 4 | description: 5 | 'represents a response from a search query, used for validating output', 6 | properties: { 7 | __typename: { type: 'string' }, 8 | id: { type: 'string' }, 9 | }, 10 | required: ['__typename', 'id'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/cli/__tests__/search.test.ts: -------------------------------------------------------------------------------- 1 | import { execCommand } from './helper'; 2 | import { query as querySchema } from './schemas/querySchema'; 3 | import { matchers } from 'jest-json-schema'; 4 | 5 | expect.extend(matchers); 6 | 7 | describe('searching', () => { 8 | const [output, error] = execCommand('search', ['hello', '--json']); 9 | 10 | expect(error).toBeNull(); 11 | 12 | const data = JSON.parse(output); 13 | 14 | it('yields valid data', () => { 15 | data.forEach((queryResult: any) => { 16 | expect(queryResult).toMatchSchema(querySchema); 17 | }); 18 | }); 19 | 20 | it('rejects incomplete requests', () => { 21 | const [_, error] = execCommand('search', ['--json']); 22 | 23 | expect(error).not.toBeNull(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/cli/__tests__/updateProfile.test.ts: -------------------------------------------------------------------------------- 1 | describe('updating a profile', () => { 2 | it.todo('applies changes'); 3 | it.todo('validates input'); 4 | it.todo('unpacks arguments properly'); 5 | it.todo('rejects unauthorized requests'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/cli/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/cli/examples/fetching-posts/fetch_posts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # this example requires jq and glow installed 3 | 4 | POSTS=$(../../bin/oasis fetch_posts --limit 5 --json | jq) 5 | 6 | for post in $(echo "${POSTS}" | jq -r '.[] | @base64'); do 7 | _jq() { 8 | echo ${post} | base64 --decode | jq -r ${1} 9 | } 10 | 11 | echo $(_jq '.message') | glow - 12 | done 13 | -------------------------------------------------------------------------------- /packages/cli/examples/image-rendering/image-rendering.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this example requires ueberzug installed 4 | # you can install it using your favorite package 5 | # manager as most linux distributions have it in 6 | # their official repositories 7 | 8 | IMAGE=$(../../bin/oasis get_post_by_id $1 --json | jq -r '.imageName') 9 | 10 | render_image() { 11 | { 12 | declare -Ap add_command=([action]="add" [identifier]="example1" [x]="10" [y]="10" [path]="/tmp/$1.jpg") 13 | sleep 120 14 | } | ueberzug layer --parser bash 15 | } 16 | 17 | if [ $IMAGE == "null" ]; then 18 | echo "This post does not have an image attached to it" 19 | else 20 | wget $IMAGE -O /tmp/$1.jpg 21 | 22 | render_image $1 23 | fi 24 | -------------------------------------------------------------------------------- /packages/cli/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | export default async (): Promise => { 4 | return { 5 | verbose: false, 6 | rootDir: './__tests__', 7 | testPathIgnorePatterns: ['./__tests__/schemas/*', './__tests__/helper.ts'], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/cli/src/bin/oasis.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | 5 | require(path.join(__dirname, '../index')).default(); 6 | -------------------------------------------------------------------------------- /packages/cli/src/commands/deleteAccount.ts: -------------------------------------------------------------------------------- 1 | // deleting accounts will not work because other documents in other tables reference 2 | // the account ID, leading to a foreign key constraint violation 3 | 4 | // import * as log from '../utils/log'; 5 | // import { gql, GraphQLClient } from 'graphql-request'; 6 | // 7 | // export async function handler(yargs: any) { 8 | // const useJSON = yargs.json ?? false; 9 | // 10 | // const client = new GraphQLClient(GQL_URL, { 11 | // headers: { 12 | // authorization: 'Bearer INSERT TOKEN HERE', 13 | // }, 14 | // }); 15 | // 16 | // const query = gql` 17 | // mutation deleteAccount { 18 | // deleteAccount 19 | // } 20 | // `; 21 | // 22 | // client.request(query).then((res) => { 23 | // if (useJSON) return console.log(JSON.stringify(res)); 24 | // log.info(res); 25 | // }); 26 | // } 27 | 28 | import * as log from '../utils/output/log'; 29 | 30 | export const handler = async () => { 31 | log.error('this command has not been implemented yet.'); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import * as log from '../utils/output/log'; 2 | 3 | export const handler = async () => { 4 | log.error('this command has not been implemented yet'); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/cli/src/commands/profile.ts: -------------------------------------------------------------------------------- 1 | import * as log from '../utils/output/log'; 2 | import { gqlURL } from '../lib/constants'; 3 | import { gql, GraphQLClient } from 'graphql-request'; 4 | 5 | export const handler = async (yargs: any) => { 6 | const useJSON = yargs.json; 7 | 8 | const client = new GraphQLClient(gqlURL, { 9 | headers: { 10 | authorization: 'Bearer INSERT TOKEN HERE', 11 | }, 12 | }); 13 | 14 | const query = gql` 15 | query { 16 | currentUser { 17 | id 18 | banner 19 | avatar 20 | createdAt 21 | github 22 | twitter 23 | discord 24 | google 25 | bio 26 | username 27 | name 28 | verified 29 | roles 30 | } 31 | } 32 | `; 33 | 34 | client.request(query).then((res) => { 35 | if (useJSON) return console.log(JSON.stringify(res)); 36 | log.info(res); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/cli/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const gqlURL = 'https://dev.oasis.sh/graphql'; 2 | -------------------------------------------------------------------------------- /packages/cli/src/types/arguments.ts: -------------------------------------------------------------------------------- 1 | export interface BaseArguments { 2 | auth: string; 3 | server: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli/src/utils/output/exit.ts: -------------------------------------------------------------------------------- 1 | import * as log from './log'; 2 | 3 | export const exit = (amount: number): any => { 4 | log.error( 5 | `${ 6 | amount >= 1 7 | ? `Exiting with ${amount} error` 8 | : `Exiting with ${amount} errors` 9 | }...` 10 | ); 11 | return process.exit(1); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/cli/src/utils/output/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const colors = { 4 | error: chalk.red('error') + ' -', 5 | ready: chalk.green('ready') + ' -', 6 | warn: chalk.yellow('warn') + ' -', 7 | event: chalk.magenta('event') + ' -', 8 | info: chalk.magenta('info') + ' -', 9 | }; 10 | 11 | // Ready, no issues 12 | export const ready = (...message: string[]) => { 13 | console.log(colors.ready, ...message); 14 | }; 15 | 16 | // Uh oh, there were issues found 17 | export const error = (...message: string[]) => { 18 | console.error(colors.error, ...message); 19 | }; 20 | 21 | export const warn = (...message: string[]) => { 22 | console.warn(colors.warn, ...message); 23 | }; 24 | 25 | export const event = (...message: string[]) => { 26 | console.log(colors.event, ...message); 27 | }; 28 | 29 | // Information 30 | export const info = (...message: string[]) => { 31 | console.log(colors.info, ...message); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2017", "es7", "es6", "dom"], 6 | "declaration": true, 7 | "outDir": "dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "rootDir": "src", 11 | "skipLibCheck": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "__tests__/**/*", 16 | "dist/**/*", 17 | "jest.config.ts", 18 | "babel.config.js" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/desktop/.env.example: -------------------------------------------------------------------------------- 1 | # Server can be local for localhost:3000, staging for dev.oasis.sh, or prod for oasis.sh, if the value is not set it will default to prod 2 | SERVER=local 3 | -------------------------------------------------------------------------------- /packages/desktop/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .env 3 | -------------------------------------------------------------------------------- /packages/desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oasis-sh/desktop", 3 | "version": "1.0.0", 4 | "description": "Oasis Desktop App", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "start": "electron ./dist/index.js", 10 | "build:mac": "yarn build && electron-builder --mac", 11 | "build:win": "yarn build && electron-builder --win", 12 | "build:linux": "yarn build && electron-builder --linux" 13 | }, 14 | "devDependencies": { 15 | "electron": "^13.1.4", 16 | "pinst": "^2.1.6", 17 | "typescript": "4.3.5" 18 | }, 19 | "dependencies": { 20 | "electron-log": "^4.3.5", 21 | "electron-updater": "^4.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon-light.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon-light.icns -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon-light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon-light.ico -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon-light.png -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon.icns -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon.ico -------------------------------------------------------------------------------- /packages/desktop/resources/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/desktop/resources/icons/icon.png -------------------------------------------------------------------------------- /packages/desktop/src/lib/links.ts: -------------------------------------------------------------------------------- 1 | export const REPO_URL = 'https://github.com/oasis-sh/oasis'; 2 | export const DISCUSSION_URL = 'https://github.com/oasis-sh/oasis/discussions'; 3 | export const ISSUES_URL = 'https://github.com/oasis-sh/oasis/issues'; 4 | -------------------------------------------------------------------------------- /packages/desktop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "baseUrl": ".", 9 | "rootDir": "src", 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "commonjs", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "noImplicitAny": false 20 | }, 21 | "include": ["src/**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/mobile/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .vscode 21 | .idea 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Optional eslint cache 28 | .eslintcache 29 | 30 | dist 31 | -------------------------------------------------------------------------------- /packages/mobile/capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "sh.oasis.oasis", 3 | "appName": "app", 4 | "webDir": "build", 5 | "bundledWebRuntime": false 6 | } 7 | -------------------------------------------------------------------------------- /packages/mobile/ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "integrations": { 4 | "capacitor": {} 5 | }, 6 | "type": "react" 7 | } 8 | -------------------------------------------------------------------------------- /packages/mobile/public/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/mobile/public/assets/icon/favicon.png -------------------------------------------------------------------------------- /packages/mobile/public/assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/mobile/public/assets/icon/icon.png -------------------------------------------------------------------------------- /packages/mobile/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /packages/mobile/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Ionic App", 3 | "name": "My Ionic App", 4 | "icons": [ 5 | { 6 | "src": "assets/icon/favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "assets/icon/icon.png", 12 | "type": "image/png", 13 | "sizes": "512x512", 14 | "purpose": "maskable" 15 | } 16 | ], 17 | "start_url": ".", 18 | "display": "standalone", 19 | "theme_color": "#ffffff", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /packages/mobile/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders without crashing', () => { 6 | const { baseElement } = render(); 7 | expect(baseElement).toBeDefined(); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/mobile/src/components/ExploreContainer.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | position: absolute; 4 | left: 0; 5 | right: 0; 6 | top: 50%; 7 | transform: translateY(-50%); 8 | } 9 | 10 | .container strong { 11 | font-size: 20px; 12 | line-height: 26px; 13 | } 14 | 15 | .container p { 16 | font-size: 16px; 17 | line-height: 22px; 18 | color: #8c8c8c; 19 | margin: 0; 20 | } 21 | 22 | .container a { 23 | text-decoration: none; 24 | } -------------------------------------------------------------------------------- /packages/mobile/src/components/ExploreContainer.tsx: -------------------------------------------------------------------------------- 1 | import './ExploreContainer.css'; 2 | 3 | interface ContainerProps {} 4 | 5 | const ExploreContainer: React.FC = () => { 6 | return ( 7 |
8 | Ready to create an app? 9 |

10 | Start with Ionic{' '} 11 | 16 | UI Components 17 | 18 |

19 |
20 | ); 21 | }; 22 | 23 | export default ExploreContainer; 24 | -------------------------------------------------------------------------------- /packages/mobile/src/index.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import ReactDOM from 'react-dom'; 3 | // import App from './App'; 4 | // import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 5 | // import './theme/globals.css'; 6 | 7 | // ReactDOM.render( 8 | // 9 | // 10 | // , 11 | // document.getElementById('root') 12 | // ); 13 | 14 | // // If you want your app to work offline and load faster, you can change 15 | // // unregister() to register() below. Note this comes with some pitfalls. 16 | // // Learn more about service workers: https://cra.link/PWA 17 | // serviceWorkerRegistration.unregister(); 18 | 19 | export {}; 20 | -------------------------------------------------------------------------------- /packages/mobile/src/pages/Home.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/mobile/src/pages/Home.css -------------------------------------------------------------------------------- /packages/mobile/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IonContent, 3 | IonHeader, 4 | IonPage, 5 | IonTitle, 6 | IonToolbar, 7 | } from '@ionic/react'; 8 | import ExploreContainer from '../components/ExploreContainer'; 9 | import './Home.css'; 10 | import { Button } from '@oasis-sh/ui'; 11 | 12 | const Home: React.FC = () => { 13 | return ( 14 | 15 | 16 | 17 | Blank 18 | 19 | 20 | 21 | 22 | 23 | Blank 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Home; 34 | -------------------------------------------------------------------------------- /packages/mobile/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /packages/mobile/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things upvote: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | 7 | // Mock matchmedia 8 | window.matchMedia = 9 | window.matchMedia || 10 | function() { 11 | return { 12 | matches: false, 13 | addListener: {}, 14 | removeListener: {}, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "outDir": "dist", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": false, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-gql/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | dist/ 3 | node_modules 4 | -------------------------------------------------------------------------------- /packages/react-gql/README.md: -------------------------------------------------------------------------------- 1 | # client-gql 2 | -------------------------------------------------------------------------------- /packages/react-gql/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: ../api/schema.gql 2 | documents: ./src/**/*.gql 3 | generates: 4 | generated/index.ts: 5 | plugins: 6 | - typescript 7 | - typescript-operations 8 | - typescript-react-apollo 9 | - typescript-document-nodes 10 | config: 11 | withHooks: true 12 | noGraphQLTag: true 13 | -------------------------------------------------------------------------------- /packages/react-gql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oasis-sh/react-gql", 3 | "version": "1.0.0", 4 | "description": "Client-Side GraphQL API Hooks for Oasis", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "gen": "graphql-codegen", 9 | "build": "graphql-codegen && tsc", 10 | "watch:gen": "graphql-codegen -w", 11 | "watch:tsc": "tsc --watch", 12 | "dev": "run-p \"watch:gen\" \"watch:tsc\"", 13 | "clean": "rimraf dist/*" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@graphql-codegen/cli": "^1.21.5", 18 | "@graphql-codegen/typescript": "^1.22.4", 19 | "@graphql-codegen/typescript-document-nodes": "^1.17.14", 20 | "@graphql-codegen/typescript-operations": "^1.18.2", 21 | "@graphql-codegen/typescript-react-apollo": "^2.2.7", 22 | "@types/rimraf": "^3.0.0", 23 | "graphql-tag": "^2.12.5", 24 | "npm-run-all": "^4.1.5", 25 | "typescript": "4.3.5" 26 | }, 27 | "dependencies": { 28 | "@apollo/client": "^3.3.20", 29 | "graphql": "^15.5.1", 30 | "rimraf": "^3.0.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-gql/src/comment/createComment.gql: -------------------------------------------------------------------------------- 1 | mutation CreateComment($data: NewCommentInput! $postId: String!) { 2 | createComment(data: $data postId: $postId) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/comment/getPostComments.gql: -------------------------------------------------------------------------------- 1 | query GetPostComments($postId: String! $commentsOffset: Float! $commentsLimit: Float!) { 2 | getPost(id: $postId) { 3 | id 4 | comments(offset: $commentsOffset limit: $commentsLimit) { 5 | items { 6 | id 7 | content 8 | upvotes 9 | downvotes 10 | createdAt 11 | lastEdited 12 | author { 13 | avatar 14 | username 15 | name 16 | } 17 | isUpvoted 18 | isDownvoted 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-gql/src/comment/getUsersComments.gql: -------------------------------------------------------------------------------- 1 | query getUsersComments( 2 | $username: String! 3 | $commentsLimit: Float! 4 | $commentsOffset: Float! 5 | ) { 6 | userOnlyComments: getUserByName(username: $username) { 7 | comments(offset: $commentsOffset, limit: $commentsLimit) { 8 | items { 9 | id 10 | content 11 | createdAt 12 | lastEdited 13 | author { 14 | id 15 | avatar 16 | username 17 | name 18 | } 19 | isUpvoted 20 | isDownvoted 21 | upvotes 22 | downvotes 23 | } 24 | hasMore 25 | total 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-gql/src/comment/likeComment.gql: -------------------------------------------------------------------------------- 1 | mutation UpvoteDownvoteComment( 2 | $upvote: Boolean! 3 | $downvote: Boolean! 4 | $commentId: String! 5 | ) { 6 | upvoteDownvoteComment(upvote: $upvote, downvote: $downvote, commentId: $commentId) 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-gql/src/comment/newComment.gql: -------------------------------------------------------------------------------- 1 | mutation MakeComment($content: String!, $postId: String!) { 2 | createComment(data: { content: $content }, postId: $postId) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/notifications/getNotifications.gql: -------------------------------------------------------------------------------- 1 | query getNotifications { 2 | getNotifications { 3 | read 4 | id 5 | performer { 6 | name 7 | avatar 8 | username 9 | } 10 | type 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-gql/src/notifications/markNotificationAsRead.gql: -------------------------------------------------------------------------------- 1 | mutation MarkNotificationAsRead($notificationId: String!) { 2 | markNotificationAsRead(notificationId: $notificationId) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/post/deletePost.gql: -------------------------------------------------------------------------------- 1 | mutation DeletePost($postId: String!) { 2 | deletePost(postId: $postId) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/post/feedSort.gql: -------------------------------------------------------------------------------- 1 | query FeedSortPosts($postsLimit: Float!, $postsOffset: Float!) { 2 | feedSortPosts(limit: $postsLimit, offset: $postsOffset) { 3 | author { 4 | id 5 | name 6 | username 7 | badges { 8 | description 9 | id 10 | imagePath 11 | } 12 | avatar 13 | } 14 | createdAt 15 | downvotes 16 | id 17 | lastEdited 18 | upvotes 19 | message 20 | resort { 21 | id 22 | description 23 | logo 24 | name 25 | } 26 | comments(limit: 0, offset: 0) { 27 | total 28 | } 29 | topics 30 | isUpvoted 31 | isDownvoted 32 | imageName 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/react-gql/src/post/getPosts.gql: -------------------------------------------------------------------------------- 1 | fragment PostFields on Post { 2 | author { 3 | id 4 | name 5 | username 6 | badges { 7 | description 8 | id 9 | imagePath 10 | } 11 | avatar 12 | } 13 | createdAt 14 | downvotes 15 | id 16 | lastEdited 17 | upvotes 18 | message 19 | resort { 20 | id 21 | description 22 | logo 23 | name 24 | } 25 | comments(limit: 0, offset: 0) { 26 | total 27 | } 28 | topics 29 | isUpvoted 30 | isDownvoted 31 | imageName 32 | } 33 | 34 | query getPost($id: String!) { 35 | getPost(id: $id) { 36 | ...PostFields 37 | } 38 | } 39 | 40 | query PaginatePosts($postsLimit: Float!, $postsOffset: Float!) { 41 | paginatePosts(limit: $postsLimit, offset: $postsOffset) { 42 | ...PostFields 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react-gql/src/post/makePost.gql: -------------------------------------------------------------------------------- 1 | mutation MakePost($message: String!, $topics: [String!]!) { 2 | createPost(data: { message: $message, topics: $topics }) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/post/upvoteDownvote.gql: -------------------------------------------------------------------------------- 1 | mutation UpvoteDownvotePost( 2 | $upvote: Boolean! 3 | $downvote: Boolean! 4 | $postId: String! 5 | ) { 6 | upvoteDownvote(upvote: $upvote, downvote: $downvote, postId: $postId) 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-gql/src/report.gql: -------------------------------------------------------------------------------- 1 | mutation ReportEntity($reportData: MakeReportInput!, $reporteeData: ReportedEntityInput!) { 2 | makeReport(data: $reportData reporteeData: $reporteeData) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/resort/getResortByName.gql: -------------------------------------------------------------------------------- 1 | query GetResortByNameWithMembers( 2 | $name: String! 3 | $membersOffset: Float! 4 | $membersLimit: Float! 5 | ) { 6 | getResortByName(name: $name) { 7 | id 8 | name 9 | description 10 | banner 11 | logo 12 | category 13 | isJoined 14 | members(offset: $membersOffset, limit: $membersLimit) { 15 | items { 16 | id 17 | name 18 | avatar 19 | } 20 | total 21 | hasMore 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-gql/src/resort/joinResort.gql: -------------------------------------------------------------------------------- 1 | mutation JoinResort($resortId: String!) { 2 | joinResort(resortId: $resortId) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-gql/src/search.gql: -------------------------------------------------------------------------------- 1 | query Search($searchQuery: String! $limit: Float!) { 2 | search(searchQuery: $searchQuery limit: $limit) { 3 | __typename 4 | ... on User { 5 | id 6 | userBanner: banner 7 | avatar 8 | createdAt 9 | github 10 | twitter 11 | discord 12 | google 13 | bio 14 | username 15 | verified 16 | roles 17 | displayName: name 18 | } 19 | ... on Post { 20 | id 21 | message 22 | author { 23 | username 24 | avatar 25 | name 26 | } 27 | comments(limit: 0 offset: 0) { 28 | total 29 | } 30 | createdAt 31 | upvotes 32 | downvotes 33 | } 34 | ... on Resort { 35 | name 36 | description 37 | banner 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-gql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "generated", 4 | "outDir": "dist", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "preserveWatchOutput": true, 8 | "skipLibCheck": true 9 | }, 10 | "include": ["generated/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | /src/generated 5 | -------------------------------------------------------------------------------- /packages/sdk/README.md: -------------------------------------------------------------------------------- 1 | # bot 2 | -------------------------------------------------------------------------------- /packages/sdk/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: ../api/schema.gql 2 | generates: 3 | src/generated/types.ts: 4 | plugins: 5 | - typescript 6 | config: 7 | constEnums: true 8 | -------------------------------------------------------------------------------- /packages/sdk/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import genArguments from './arguments'; 2 | import listAll from './listAll'; 3 | import mainClient from './main'; 4 | import relations from './relations'; 5 | 6 | (async () => { 7 | await mainClient(); 8 | await relations(); 9 | await listAll(); 10 | await genArguments(); 11 | })(); 12 | -------------------------------------------------------------------------------- /packages/sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { gqlURL } from './lib/constants'; 2 | 3 | export const API_BASE_URL = 4 | process.env.NODE_ENV === 'production' 5 | ? 'https://oasis.sh' 6 | : process.env.API_BASE_URL ?? gqlURL; 7 | 8 | -------------------------------------------------------------------------------- /packages/sdk/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import './test-utils/setup'; 2 | import { Client } from '.'; 3 | 4 | const client = new Client({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | test('gets `currentUser` data', async () => { 9 | const data = await client 10 | .createQueryBuilder('currentUser') 11 | .addFields({ 12 | id: true, 13 | isBot: true, 14 | answers: { 15 | ARGS: { 16 | limit: 50, 17 | offset: 0, 18 | }, 19 | items: { 20 | id: true, 21 | }, 22 | }, 23 | }) 24 | .send(); 25 | 26 | expect(data).toBeTruthy(); 27 | expect(data).toBeInstanceOf(Object); 28 | }); 29 | 30 | // describe('Nothing', () => { 31 | // it('pass', () => { 32 | // expect(true).toBe(true); 33 | // }); 34 | // }); 35 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated/client'; 2 | export * from './variable-type'; 3 | -------------------------------------------------------------------------------- /packages/sdk/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const gqlURL = 'https://dev.oasis.sh/graphql'; 2 | -------------------------------------------------------------------------------- /packages/sdk/src/test-utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | config(); 4 | -------------------------------------------------------------------------------- /packages/sdk/src/test.ts: -------------------------------------------------------------------------------- 1 | import './test-utils/setup'; 2 | import { Client } from '.'; 3 | import { variable } from './variable-type'; 4 | 5 | const client = new Client({ 6 | token: process.env.TOKEN, 7 | }); 8 | 9 | (async () => { 10 | // const data = await client.post.paginate({ limit: 2, offset: 0 }, [ 11 | // 'id', 12 | // 'message', 13 | // ]); 14 | 15 | const data = await client 16 | .createQueryBuilder('paginatePosts') 17 | .addFields('id', 'message') 18 | .args({ 19 | limit: variable('lim'), 20 | offset: variable('off'), 21 | }) 22 | .send({ lim: 2, off: 0 }); 23 | 24 | console.log(data); 25 | })(); 26 | 27 | // describe('Nothing', () => { 28 | // it('pass', () => { 29 | // expect(true).toBe(true); 30 | // }); 31 | // }); 32 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/output/exit.ts: -------------------------------------------------------------------------------- 1 | import * as log from './log'; 2 | 3 | export const exit = (amount: number): any => { 4 | log.error( 5 | `${ 6 | amount >= 1 7 | ? `Exiting with ${amount} error` 8 | : `Exiting with ${amount} errors` 9 | }...` 10 | ); 11 | return process.exit(1); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/output/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const colors = { 4 | error: chalk.red('error') + ' -', 5 | ready: chalk.green('ready') + ' -', 6 | warn: chalk.yellow('warn') + ' -', 7 | event: chalk.magenta('event') + ' -', 8 | info: chalk.magenta('info') + ' -', 9 | }; 10 | 11 | // Ready, no issues 12 | export const ready = (...message: string[]) => { 13 | console.log(colors.ready, ...message); 14 | }; 15 | 16 | // Uh oh, there were issues found 17 | export const error = (...message: string[]) => { 18 | console.error(colors.error, ...message); 19 | }; 20 | 21 | export const warn = (...message: string[]) => { 22 | console.warn(colors.warn, ...message); 23 | }; 24 | 25 | export const event = (...message: string[]) => { 26 | console.log(colors.event, ...message); 27 | }; 28 | 29 | // Information 30 | export const info = (...message: string[]) => { 31 | console.log(colors.info, ...message); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/sdk/src/variable-type.ts: -------------------------------------------------------------------------------- 1 | export class VariableType { 2 | constructor(public name: string) {} 3 | } 4 | 5 | export const variable = (name: string): any => new VariableType(name); 6 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "dist", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": ["ESNext"], 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "declaration": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/tui/.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /packages/tui/README.md: -------------------------------------------------------------------------------- 1 | # tui 2 | 3 | ## Building 4 | 5 | `go build -o ./dist/ ./cmd/termoasis` 6 | 7 | ## Running 8 | 9 | `./dist/termoasis` 10 | 11 | ## Contributing 12 | 13 | Coming Soon... 14 | -------------------------------------------------------------------------------- /packages/tui/cmd/termoasis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | models "github.com/oasis-sh/oasis/packages/tui/internal/model" 10 | posts "github.com/oasis-sh/oasis/packages/tui/internal/queries" 11 | "github.com/shurcooL/graphql" 12 | ) 13 | 14 | func main() { 15 | client := graphql.NewClient("https://dev.oasis.sh/graphql", nil) 16 | // client := graphql.NewClient("http://localhost:3000/graphql", nil) 17 | 18 | posts, err := posts.FetchPosts(client, 10.0, 0.0) 19 | 20 | if err != nil { 21 | log.Fatal(err) 22 | os.Exit(1) 23 | } 24 | 25 | p := tea.NewProgram( 26 | models.Model{Client: client, Posts: posts, Read: 10}, 27 | tea.WithAltScreen(), 28 | tea.WithMouseCellMotion(), 29 | ) 30 | 31 | if err := p.Start(); err != nil { 32 | fmt.Println("could not run program:", err) 33 | os.Exit(1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/tui/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oasis-sh/oasis/packages/tui 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.8.0 7 | github.com/charmbracelet/bubbletea v0.14.0 8 | github.com/charmbracelet/glamour v0.3.0 9 | github.com/charmbracelet/lipgloss v0.2.1 10 | github.com/muesli/reflow v0.3.0 // indirect 11 | github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a 12 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /packages/tui/internal/posts/author/render.go: -------------------------------------------------------------------------------- 1 | package termoasis 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | type Author struct { 6 | Username string 7 | } 8 | 9 | func (a Author) Render() string { 10 | AuthorStyle := lipgloss.NewStyle().Bold(true).Align(lipgloss.Left).Blink(true) 11 | rendered := AuthorStyle.Render(a.Username) 12 | return rendered 13 | } 14 | -------------------------------------------------------------------------------- /packages/tui/internal/posts/date/render.go: -------------------------------------------------------------------------------- 1 | package termoasis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type Date struct { 10 | Timestamp time.Time 11 | } 12 | 13 | func (d Date) Render() string { 14 | DateStyle := lipgloss.NewStyle().Bold(true) 15 | formattedTime := d.Timestamp.Format("2006-01-02 03:04") 16 | return DateStyle.Render(formattedTime) 17 | } 18 | -------------------------------------------------------------------------------- /packages/tui/internal/posts/downvotes/render.go: -------------------------------------------------------------------------------- 1 | package termoasis 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type Downvotes struct { 10 | Amount int 11 | } 12 | 13 | func (dv Downvotes) Render() string { 14 | DownvotesStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("111")) 15 | 16 | return DownvotesStyle.Render(fmt.Sprint(dv.Amount)) 17 | } 18 | -------------------------------------------------------------------------------- /packages/tui/internal/posts/upvotes/render.go: -------------------------------------------------------------------------------- 1 | package termoasis 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type Upvotes struct { 10 | Amount int 11 | } 12 | 13 | func (uv Upvotes) Render() string { 14 | UpvotesStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("154")) 15 | 16 | return UpvotesStyle.Render(fmt.Sprint(uv.Amount)) 17 | } 18 | -------------------------------------------------------------------------------- /packages/tui/internal/utils/date.go: -------------------------------------------------------------------------------- 1 | package termoasis 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/shurcooL/graphql" 8 | ) 9 | 10 | func ConvertToTime(rawTimestamp graphql.String) (*time.Time, error) { 11 | timestamp, err := strconv.ParseInt(string(rawTimestamp), 10, 64) 12 | if err != nil { 13 | return nil, err 14 | } 15 | date := time.Unix(0, timestamp*int64(time.Millisecond)) 16 | return &date, nil 17 | } 18 | -------------------------------------------------------------------------------- /packages/vsc-extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | -------------------------------------------------------------------------------- /packages/vsc-extension/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vsc-extension/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/vsc-extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "npm.packageManager": "yarn" 12 | } 13 | -------------------------------------------------------------------------------- /packages/vsc-extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/vsc-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | -------------------------------------------------------------------------------- /packages/vsc-extension/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /packages/vsc-extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vsc-oasis" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /packages/vsc-extension/media/reset.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/vsc-extension/media/reset.css -------------------------------------------------------------------------------- /packages/vsc-extension/media/vscode.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/vsc-extension/media/vscode.css -------------------------------------------------------------------------------- /packages/vsc-extension/out/extension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.deactivate = exports.activate = void 0; 4 | const vscode = require("vscode"); 5 | const Sidebar_1 = require("./Sidebar"); 6 | function activate(context) { 7 | console.log('Congratulations, your extension "vsc-oasis" is now active!'); 8 | const sidebarProvider = new Sidebar_1.SidebarProvider(context); 9 | context.subscriptions.push(vscode.window.registerWebviewViewProvider('vsc-oasis-sidebar', sidebarProvider)); 10 | context.subscriptions.push(vscode.commands.registerCommand('vsc-oasis.helloWorld', () => { 11 | vscode.window.showInformationMessage('Hello World from Oasis!'); 12 | })); 13 | } 14 | exports.activate = activate; 15 | // eslint-disable-next-line @typescript-eslint/no-empty-function 16 | function deactivate() { } 17 | exports.deactivate = deactivate; 18 | //# sourceMappingURL=extension.js.map -------------------------------------------------------------------------------- /packages/vsc-extension/out/extension.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;;AAAA,iCAAiC;AACjC,uCAA4C;AAE5C,SAAgB,QAAQ,CAAC,OAAgC;IACvD,OAAO,CAAC,GAAG,CAAC,4DAA4D,CAAC,CAAC;IAE1E,MAAM,eAAe,GAAG,IAAI,yBAAe,CAAC,OAAO,CAAC,CAAC;IACrD,OAAO,CAAC,aAAa,CAAC,IAAI,CACxB,MAAM,CAAC,MAAM,CAAC,2BAA2B,CACvC,mBAAmB,EACnB,eAAe,CAChB,CACF,CAAC;IAEF,OAAO,CAAC,aAAa,CAAC,IAAI,CACxB,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,yBAAyB,CAAC,CAAC;IAClE,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAhBD,4BAgBC;AAED,gEAAgE;AAChE,SAAgB,UAAU,KAAU,CAAC;AAArC,gCAAqC"} -------------------------------------------------------------------------------- /packages/vsc-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SidebarProvider } from './Sidebar'; 3 | 4 | export function activate(context: vscode.ExtensionContext): void { 5 | console.log('Congratulations, your extension "vsc-oasis" is now active!'); 6 | 7 | const sidebarProvider = new SidebarProvider(context); 8 | context.subscriptions.push( 9 | vscode.window.registerWebviewViewProvider( 10 | 'vsc-oasis-sidebar', 11 | sidebarProvider 12 | ) 13 | ); 14 | 15 | context.subscriptions.push( 16 | vscode.commands.registerCommand('vsc-oasis.helloWorld', () => { 17 | vscode.window.showInformationMessage('Hello World from Oasis!'); 18 | }) 19 | ); 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-empty-function 23 | export function deactivate(): void {} 24 | -------------------------------------------------------------------------------- /packages/vsc-extension/src/getNonce.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() { 2 | let text = ''; 3 | const possible = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /packages/vsc-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "types": ["node"], 12 | "strict": true /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": ["node_modules", ".vscode-test", "webviews", "server"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/vsc-extension/webviews/pages/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | ReactDOM.render(

Hello

, document.getElementById('root')); 5 | -------------------------------------------------------------------------------- /packages/vsc-extension/webviews/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "moduleResolution": "node", 5 | "target": "es2017", 6 | "importsNotUsedAsValues": "error", 7 | "isolatedModules": true, 8 | "sourceMap": true, 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "lib": ["DOM"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 7 | ["@babel/plugin-proposal-class-properties", { "loose": true }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/.env.example: -------------------------------------------------------------------------------- 1 | # Use the Oasis Staging API (Optional) 2 | STAGING_API=true 3 | 4 | # Enable Bundle Analyzer (Optional) 5 | ANALYZE=true 6 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # next.js 5 | /.next/ 6 | ormconfig.ts 7 | 8 | # debug 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # local env files 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # sql 20 | *.sqlite 21 | 22 | /server/dist 23 | 24 | # cypres 25 | cypress/screenshots 26 | -------------------------------------------------------------------------------- /packages/web/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/web/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | controls: { 5 | matchers: { 6 | color: /(background|color)$/i, 7 | date: /Date$/, 8 | }, 9 | }, 10 | } -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | # web 2 | -------------------------------------------------------------------------------- /packages/web/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "defaultCommandTimeout": 20000, 4 | "env": { 5 | "API_MODE": "remote" 6 | }, 7 | "video": false, 8 | "screenshotOnRunFailure": false 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /packages/web/cypress/integration/main.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import { loginWithTesting } from '../utils/loginAsTesting'; 4 | 5 | context('Feed page', () => { 6 | beforeEach(() => { 7 | loginWithTesting(); 8 | }); 9 | 10 | it('Navbar has user info', () => { 11 | cy.visit('/'); 12 | cy.get('#navbar-user-avatar').should('have.attr', 'alt', '@testing'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/web/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const preprocessor = require('./preprocessor'); 2 | 3 | module.exports = (on) => { 4 | on('file:preprocessor', preprocessor); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/web/cypress/plugins/preprocessor.js: -------------------------------------------------------------------------------- 1 | const wp = require('@cypress/webpack-preprocessor'); 2 | 3 | const webpackOptions = { 4 | resolve: { 5 | extensions: ['.ts', '.js'], 6 | }, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.ts$/, 11 | exclude: [/node_modules/], 12 | use: [ 13 | { 14 | loader: 'ts-loader', 15 | }, 16 | ], 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | const options = { 23 | webpackOptions, 24 | }; 25 | 26 | module.exports = wp(options); 27 | -------------------------------------------------------------------------------- /packages/web/cypress/support/index.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /packages/web/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es6", 5 | "jsx": "preserve", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "lib": ["ESNext", "DOM"], 9 | "types": ["cypress"], 10 | "module": "commonjs", 11 | "esModuleInterop": true, 12 | "preserveSymlinks": true, 13 | "typeRoots": ["./node_modules/@types"], 14 | "downlevelIteration": true, 15 | "allowJs": true, 16 | }, 17 | "include": ["./**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/web/cypress/utils/loginAsTesting.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | export const loginWithTesting = (username = 'testing') => { 4 | cy.intercept('localhost:3000', (req) => { 5 | req.headers.authorization = `TESTING ${username}`; 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Oasis", 3 | "short_name": "Oasis", 4 | "icons": [ 5 | { 6 | "src": "static/favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "static/favicons/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "theme_color": "#673ab7", 19 | "background_color": "#EEE" 20 | } 21 | -------------------------------------------------------------------------------- /packages/web/public/static/DiscordLogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/web/public/static/GenericUser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/web/public/static/OasisLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/OasisLogo.png -------------------------------------------------------------------------------- /packages/web/public/static/badges/contributor-badge.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/web/public/static/badges/dunce-cap.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/web/public/static/badges/verified-badge.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/web/public/static/default-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/default-banner.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/android-chrome-256x256.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/web/public/static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/favicon.ico -------------------------------------------------------------------------------- /packages/web/public/static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /packages/web/public/static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /packages/web/public/static/fonts/VictorMono-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/fonts/VictorMono-Regular.eot -------------------------------------------------------------------------------- /packages/web/public/static/fonts/VictorMono-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/fonts/VictorMono-Regular.otf -------------------------------------------------------------------------------- /packages/web/public/static/fonts/VictorMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/fonts/VictorMono-Regular.ttf -------------------------------------------------------------------------------- /packages/web/public/static/fonts/VictorMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/fonts/VictorMono-Regular.woff -------------------------------------------------------------------------------- /packages/web/public/static/hack-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heybereket/oasis/68d0e9602951653ff8dbfdac54a7ca198ced190c/packages/web/public/static/hack-regular-webfont.woff2 -------------------------------------------------------------------------------- /packages/web/server/src/config/database.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions, createConnection } from 'typeorm'; 2 | import { joinRoot } from '../utils/rootPath'; 3 | import * as log from '../utils/output/log'; 4 | 5 | export const getDatabase = async () => { 6 | let ormconfig: ConnectionOptions; 7 | 8 | if (process.env.TESTING === 'true') { 9 | ormconfig = { 10 | type: 'sqlite', 11 | database: 'testing.sqlite', 12 | entities: [joinRoot('./entities/*.*')], 13 | synchronize: true, 14 | }; 15 | } else { 16 | ormconfig = require('../ormconfig').default; 17 | } 18 | 19 | try { 20 | await createConnection(ormconfig); 21 | log.event(`successfully connected to ${ormconfig.type} database`); 22 | } catch (err) { 23 | log.error(`failed to connect to database: ${err}`); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/web/server/src/getServer.ts: -------------------------------------------------------------------------------- 1 | import express, { Express } from 'express'; 2 | 3 | export const getServer = async (): Promise => { 4 | const app = express(); 5 | 6 | if (!process.env.STAGING_API) { 7 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 8 | // @ts-ignore 9 | return import('@oasis-sh/api').then( 10 | async ({ initializeServer }) => await initializeServer() 11 | ); 12 | } 13 | 14 | return app; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/web/server/src/utils/output/exit.ts: -------------------------------------------------------------------------------- 1 | import * as log from './log'; 2 | 3 | export const exit = (amount: number): any => { 4 | log.error( 5 | `${ 6 | amount >= 1 7 | ? `Exiting with ${amount} error` 8 | : `Exiting with ${amount} errors` 9 | }...` 10 | ); 11 | return process.exit(1); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/web/server/src/utils/output/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const colors = { 4 | error: chalk.red('error') + ' -', 5 | ready: chalk.green('ready') + ' -', 6 | warn: chalk.yellow('warn') + ' -', 7 | event: chalk.magenta('event') + ' -', 8 | info: chalk.magenta('info') + ' -', 9 | }; 10 | 11 | // Ready, no issues 12 | export const ready = (...message: string[]) => { 13 | console.log(colors.ready, ...message); 14 | }; 15 | 16 | // Uh oh, there were issues found 17 | export const error = (...message: string[]) => { 18 | console.error(colors.error, ...message); 19 | }; 20 | 21 | export const warn = (...message: string[]) => { 22 | console.warn(colors.warn, ...message); 23 | }; 24 | 25 | export const event = (...message: string[]) => { 26 | console.log(colors.event, ...message); 27 | }; 28 | 29 | // Information 30 | export const info = (...message: string[]) => { 31 | console.log(colors.info, ...message); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/web/server/src/utils/rootPath.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const rootPath = join(__dirname, '../../../dist'); 4 | export const joinRoot = (...paths: string[]) => join(rootPath, ...paths); 5 | -------------------------------------------------------------------------------- /packages/web/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "dist", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": ["ESNext"], 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "declaration": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/web/src/components/home/FollowUser.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../shared/Button'; 3 | 4 | interface Props { 5 | name: string; 6 | username: string; 7 | } 8 | 9 | export const FollowUser: React.FC = ({ name, username }) => { 10 | return ( 11 |
12 |
13 |
14 |
15 |

{name}

16 |

@{username}

17 |
18 |
19 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/web/src/components/home/FollowUserSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Sidebar } from './Sidebar'; 3 | import { FollowUser } from './FollowUser'; 4 | 5 | export const FollowUserSection: React.FC = () => { 6 | return ( 7 | 8 |
9 | 10 | 11 | 12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/web/src/components/home/FriendActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | name: string; 5 | activity: Array; 6 | avatarUrl?: string; 7 | } 8 | 9 | export const FriendActivity: React.FC = ({ 10 | name, 11 | activity, 12 | avatarUrl, 13 | }) => { 14 | return ( 15 |
16 | {avatarUrl ? ( 17 | 18 | ) : ( 19 |
20 | )} 21 |
22 |

{name}

23 |
24 | {activity && 25 | activity.map((item: string, index: number) => 26 | index % 2 === 0 ? ( 27 |

{item}

28 | ) : ( 29 |

30 | {item} 31 |

32 | ) 33 | )} 34 |
35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/web/src/components/home/FriendActivitySection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Sidebar } from '../home/Sidebar'; 3 | import { FriendActivity } from '../home/FriendActivity'; 4 | export const FriendActivitySection: React.FC = () => { 5 | return ( 6 | 7 |
8 | 12 | 16 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/web/src/components/home/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ItemProps { 4 | name: string; 5 | icon: any; 6 | active: boolean; 7 | } 8 | 9 | interface TitleProps { 10 | name: string; 11 | } 12 | 13 | export const LeftSidebarItem: React.FC = ({ 14 | name, 15 | icon, 16 | active, 17 | }) => { 18 | return ( 19 |
24 | {icon} 25 | 26 | {name} 27 | 28 |
29 | ); 30 | }; 31 | 32 | export const LeftSidebarTitle: React.FC = ({ name }) => { 33 | return ( 34 |

{name}

35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/web/src/components/home/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | title: string; 5 | children?: React.ReactNode; 6 | className?: string; 7 | } 8 | 9 | export const Sidebar: React.FC = ({ title, children, className }) => { 10 | return ( 11 |
14 |

{title}

15 | {children} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/web/src/components/home/TrendingSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Sidebar } from '../home/Sidebar'; 3 | import { TopicBadge } from '../profile/TopicBadge'; 4 | 5 | export const TrendingSection: React.FC = () => { 6 | return ( 7 | <> 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/web/src/components/locales/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { 4 | defaultLanguage, 5 | languages, 6 | useSetLanguage, 7 | } from '../../locales/LocalesProvider'; 8 | 9 | export const LanguageSelector = () => { 10 | const { i18n } = useTranslation(); 11 | 12 | const changeLang = useSetLanguage(); 13 | 14 | const handleLanguageChanged = ({ target: { value } }: any) => { 15 | changeLang( 16 | languages.find((val) => val.langCode === value) ?? defaultLanguage 17 | ); 18 | }; 19 | 20 | return ( 21 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/web/src/components/navbar/DropdownItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react'; 2 | 3 | interface DropdownItemProps { 4 | name: string; 5 | href?: string; 6 | onClick?: MouseEventHandler; 7 | icon: React.FC>; 8 | } 9 | 10 | export const DropdownItem: React.FC = (props) => { 11 | const Icon = props.icon; 12 | 13 | return ( 14 |
18 | 19 |

{props.name}

20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/web/src/components/navbar/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CustomLink } from '../../providers/CustomLink'; 3 | 4 | interface NavItemProps { 5 | name: string; 6 | href?: string; 7 | to?: string; 8 | onClick: () => void; 9 | icon: React.FC>; 10 | className: string; 11 | } 12 | 13 | export const NavItem: React.FC = (props: NavItemProps) => { 14 | const Icon = props.icon; 15 | const onClick = props.onClick; 16 | const onClickRedirect = (_: any) => { 17 | onClick(); 18 | }; 19 | 20 | return ( 21 |
25 | 26 | 30 | {props.name} 31 | 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/web/src/components/notifications/FilterButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../shared/Button'; 3 | 4 | interface Props { 5 | text: string; 6 | } 7 | 8 | export const FilterButton: React.FC = ({ text }) => { 9 | return ; 10 | }; 11 | 12 | export default FilterButton; 13 | -------------------------------------------------------------------------------- /packages/web/src/components/notifications/NotificationWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | children: React.ReactNode; 5 | } 6 | 7 | export const NotificationWrapper: React.FC = ({ 8 | children, 9 | ...props 10 | }) => { 11 | return ( 12 |
13 |
14 |

Notifications

15 |
16 | {children} 17 |
18 | ); 19 | }; 20 | 21 | export default NotificationWrapper; 22 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/ProfileBanner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type ProfileBannerProps = { 4 | bannerUrl: string | null | undefined; 5 | }; 6 | 7 | export const ProfileBanner: React.FC = ({ bannerUrl }) => { 8 | return ( 9 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/TabItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react'; 2 | 3 | interface NavItemProps { 4 | name: string; 5 | active: boolean; 6 | onClick?: MouseEventHandler; 7 | icon: React.FC>; 8 | } 9 | 10 | export const TabItem: React.FC = (props: NavItemProps) => { 11 | return ( 12 |
18 | 19 |
{props.name}
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/TabMeta.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | title?: string; 5 | description?: string | any; 6 | } 7 | 8 | const TabMeta: React.FC = ({ title, description }) => { 9 | return ( 10 | <> 11 | {title &&

{title}

} 12 | {description && ( 13 |
{description}
14 | )} 15 | 16 | ); 17 | }; 18 | 19 | export default TabMeta; 20 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/TopicBadge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface TopicBadgeProps { 4 | content: string; 5 | } 6 | 7 | export const TopicBadge: React.FC = (props) => { 8 | return ( 9 |
10 | {props.content} 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/large/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CustomLink } from '../../../providers/CustomLink'; 3 | 4 | type Props = { 5 | avatar: string | undefined | null; 6 | username: string | undefined | null; 7 | name: string | undefined | null; 8 | }; 9 | 10 | export const LargeUserCard: React.FC = ({ avatar, name, username }) => { 11 | return ( 12 |
13 | 14 | 19 | 20 |
21 | {name ? ( 22 | <> 23 |

{name}

24 |

@{username}

25 | 26 | ) : ( 27 |

@{username}

28 | )} 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/web/src/components/profile/small/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | avatar: string | undefined | null; 5 | username: string | undefined | null; 6 | name: string | undefined | null; 7 | }; 8 | 9 | export const SmallUserCard: React.FC = ({ avatar, name, username }) => { 10 | return ( 11 |
12 | 16 |
17 | {name ? ( 18 | <> 19 |

{name}

20 |
21 | @{username} 22 |
23 | 24 | ) : ( 25 |

@{username}

26 | )} 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/web/src/components/resort/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IAvatarGroupProps { 4 | avatarIcons: string[]; 5 | } 6 | 7 | const AvatarGroup: React.FC = ({ avatarIcons }) => { 8 | return ( 9 |
10 | {avatarIcons.map((icon) => ( 11 | avatar 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | export default AvatarGroup; 24 | -------------------------------------------------------------------------------- /packages/web/src/components/resort/ResortCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ResortCard: React.FC = () => { 4 | return ( 5 | <> 6 |
9 | something 10 |
11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/web/src/components/shared/AutoResizeTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, TextareaHTMLAttributes } from 'react'; 2 | 3 | type Props = { 4 | maxHeight?: number; 5 | }; 6 | 7 | export const AutoResizeTextArea: React.FC< 8 | Props & TextareaHTMLAttributes 9 | > = ({ maxHeight, ...props }) => { 10 | const textAreaRef = useRef(); 11 | const handleKeyDown = () => { 12 | if (textAreaRef.current) { 13 | textAreaRef.current.style.height = 'inherit'; 14 | textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; 15 | // In case you have a limitation 16 | textAreaRef.current.style.height = `${Math.min( 17 | textAreaRef.current.scrollHeight, 18 | maxHeight ?? Number.MAX_SAFE_INTEGER 19 | )}px`; 20 | } 21 | }; 22 | 23 | return ( 24 |