├── .gitattributes
├── .github
└── workflows
│ ├── go.yaml
│ └── publish.yaml
├── .gitignore
├── .slsa-goreleaser
├── darwin-amd64.yml
├── darwin-arm64.yml
├── linux-amd64.yml
├── linux-arm64.yml
├── windows-amd64.yml
└── windows-arm64.yml
├── .vscode
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── cli
├── cmd
│ ├── auth.go
│ ├── delete.go
│ ├── pastes.go
│ ├── root.go
│ └── upload.go
├── config
│ ├── config.go
│ └── fileTypes.go
├── main.go
└── supabase
│ └── supabase.go
├── go.mod
├── go.sum
├── install.ps1
├── install.sh
├── main.go
├── openbin.code-workspace
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── supabase
├── .gitignore
├── config.toml
├── seed.sql
└── types.ts
└── web
├── .eslintrc.json
├── .gitignore
├── README.md
├── _eslintrc.cjs
├── components.json
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.cjs
├── public
└── assets
│ ├── favicon.png
│ ├── favicon.svg
│ ├── image.png
│ ├── pfp-placeholder.png
│ └── profile-bg.png
├── src
├── app
│ ├── auth
│ │ ├── callback
│ │ │ ├── [redirect]
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── delete
│ │ │ ├── confirm
│ │ │ │ └── route.ts
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ └── signout
│ │ │ └── route.ts
│ ├── avatar
│ │ └── route.tsx
│ ├── editor
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── globals.css
│ ├── install.ps1
│ │ └── route.ts
│ ├── install.sh
│ │ └── route.ts
│ ├── layout.tsx
│ ├── login
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── me
│ │ └── route.ts
│ ├── page.tsx
│ ├── pastes
│ │ └── [id]
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── not-found.tsx
│ │ │ └── page.tsx
│ ├── privacy-policy
│ │ └── page.tsx
│ └── profiles
│ │ └── [id]
│ │ ├── error.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── not-found.tsx
│ │ └── page.tsx
├── assets
│ ├── bg.svg
│ ├── gh-light.json
│ ├── grid.svg
│ └── languages.json
├── components
│ ├── avatar.tsx
│ ├── editor
│ │ ├── actions.ts
│ │ ├── delete-paste.tsx
│ │ ├── index.tsx
│ │ ├── navbar.tsx
│ │ ├── publish-form.tsx
│ │ ├── toggle-publish.tsx
│ │ └── viewer.tsx
│ ├── footer.tsx
│ ├── loading-dots.module.css
│ ├── loading-dots.tsx
│ ├── loading-spinner.module.css
│ ├── loading-spinner.tsx
│ ├── login.tsx
│ ├── logo.tsx
│ ├── navbar.tsx
│ ├── svg
│ │ ├── circles.tsx
│ │ └── supabase-logo.tsx
│ ├── terminal.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── checkbox.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── separator.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
├── middleware.ts
└── utils
│ ├── cn.ts
│ ├── config.ts
│ ├── os.ts
│ └── supabase.ts
├── tailwind.config.js
├── tsconfig.json
└── types
├── supabase.ts
└── types.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/go.yaml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v4
18 | with:
19 | go-version: '1.20'
20 |
21 | - name: Build
22 | run: go build -v -o build/ .
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 |
6 | # This workflow lets you compile your Go project using a SLSA3 compliant builder.
7 | # This workflow will generate a so-called "provenance" file describing the steps
8 | # that were performed to generate the final binary.
9 | # The project is an initiative of the OpenSSF (openssf.org) and is developed at
10 | # https://github.com/slsa-framework/slsa-github-generator.
11 | # The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier.
12 | # For more information about SLSA and how it improves the supply-chain, visit slsa.dev.
13 |
14 | name: SLSA Go releaser
15 | on:
16 | workflow_dispatch:
17 | release:
18 | types: [created]
19 |
20 | permissions: read-all
21 |
22 | jobs:
23 | # ========================================================================================================================================
24 | # Prerequesite: Create a .slsa-goreleaser.yml in the root directory of your project.
25 | # See format in https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/go/README.md#configuration-file
26 | #=========================================================================================================================================
27 | build:
28 | permissions:
29 | id-token: write # To sign.
30 | contents: write # To upload release assets.
31 | actions: read # To read workflow path.
32 | strategy:
33 | matrix:
34 | os:
35 | - linux
36 | - windows
37 | - darwin
38 | arch:
39 | - amd64
40 | - arm64
41 | uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v2.1.0
42 | with:
43 | go-version: 1.19.4
44 | config-file: .slsa-goreleaser/${{ matrix.os }}-${{ matrix.arch }}.yml
45 | # =============================================================================================================
46 | # Optional: For more options, see https://github.com/slsa-framework/slsa-github-generator#golang-projects
47 | # =============================================================================================================
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | build/
3 | web/.env
4 |
--------------------------------------------------------------------------------
/.slsa-goreleaser/darwin-amd64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: darwin
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: amd64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}
--------------------------------------------------------------------------------
/.slsa-goreleaser/darwin-arm64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: darwin
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: arm64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}
--------------------------------------------------------------------------------
/.slsa-goreleaser/linux-amd64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: linux
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: amd64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}
30 |
--------------------------------------------------------------------------------
/.slsa-goreleaser/linux-arm64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: linux
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: arm64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}
--------------------------------------------------------------------------------
/.slsa-goreleaser/windows-amd64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: windows
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: amd64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}.exe
--------------------------------------------------------------------------------
/.slsa-goreleaser/windows-arm64.yml:
--------------------------------------------------------------------------------
1 | # Version for this file.
2 | version: 1
3 |
4 | # (Optional) List of env variables used during compilation.
5 | env:
6 | - GO111MODULE=on
7 | - CGO_ENABLED=0
8 |
9 | # (Optional) Flags for the compiler.
10 | flags:
11 | - -trimpath
12 | - -tags=netgo
13 |
14 | # The OS to compile for. `GOOS` env variable will be set to this value.
15 | goos: windows
16 |
17 | # The architecture to compile for. `GOARCH` env variable will be set to this value.
18 | goarch: arm64
19 |
20 | # (Optional) Entrypoint to compile.
21 | # main: ./path/to/main.go
22 |
23 | # (Optional) Working directory. (default: root of the project)
24 | # dir: ./relative/path/to/dir
25 |
26 | # Binary output name.
27 | # {{ .Os }} will be replaced by goos field in the config file.
28 | # {{ .Arch }} will be replaced by goarch field in the config file.
29 | binary: openbin-{{ .Os }}-{{ .Arch }}.exe
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
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 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch Package",
9 | "type": "go",
10 | "request": "launch",
11 | "mode": "auto",
12 | "program": "cli-go/main.go",
13 | "args": ["delete", "36468d53-525b-46d5-b9d3-973e5da362f7"]
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Unnamed Engineering
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Openbin
2 |
3 | Openbin is a Pastebin clone that takes notes & code sharing to the next level, by taking advantage of both a CLI and a web editor. We built it for the 8th Supabase Launch Week hackathon, attempting to solve issues we face often while trying to share snippets and code with friends and colleagues. The app is built using Go for the CLI and Next.js, Tailwind, TypeScript and `shadcn/ui` for the web editor. We take advantage of a whole lot of Supabase products, including the Database, Auth, Storage and the brand new Resend email integration.
4 |
5 | # Documentation
6 | The documentation is designed to help or refer you to the Openbin CLI. Occasionally, errors may occur, you may encounter bugs, or you simply have questions or need assistance in using it - if you are in one of these situations, [simply open an issue on the repo](https://github.com/ethndotsh/openbin/issues/new)!
7 |
8 | To view the complete list of options not mentioned in the documentation: `openbin up --help`.
9 |
10 | ## Installation
11 | Without guessing, this is the most important step in using CLI. You can install it on any operating system, whether Windows, Linux or macOS.
12 |
13 | ### Windows (Powershell)
14 | ```powershell
15 | irm https://openbin.ethn.sh/install.ps1 | iex
16 | ```
17 |
18 | ### Linux and macOS
19 | ```shell
20 | curl -fsSL https://openbin.ethn.sh/install.sh | sh
21 | ```
22 |
23 | To make sure that Openbin is installed, enter `openbin --version` in your terminal. If it gives you the version, this means that Openbin has been successfully installed and you're ready to start using it! 🎉
24 |
25 | ## Login to your account
26 | To get your pastes into your account, you should login to your Openbin account by entering this command:
27 | ```
28 | openbin login
29 | ```
30 | or with OAuth providers:
31 | ```
32 | openbin login -p github/gitlab/bitbucket
33 | ```
34 |
35 | Also, if you want to logout of your account, all you have to do is `openbin logout` - it's that simple!
36 | > Please note that you can't upload without being logged in, so this step is necessary.
37 |
38 | ## Upload a file to Openbin
39 |
40 | ```
41 | openbin upload [file.extension]
42 | ```
43 | ### Options:
44 | `--title [value]`\
45 | `--description [value]`\
46 | `--language [value]`\
47 | `--expire [value]`\
48 | `--editor [value]`\
49 | `--draft`
50 |
51 | ## Manage your pastes
52 | The CLI lets you do everything just like with the web editor, and that means you can manage your pastes too!
53 |
54 | ### Get a list of the pastes you've created
55 | ```
56 | openbin pastes
57 | ```
58 | ### Delete a paste
59 | ```
60 | openbin delete [uuid]
61 | ```
62 | > The UUID is located directly after the pastes/.
63 |
--------------------------------------------------------------------------------
/cli/cmd/auth.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | sb "github.com/jackmerrill/supabase-go"
9 | "github.com/urfave/cli/v2"
10 | )
11 |
12 | var LoginCommand = cli.Command{
13 | Name: "login",
14 | Aliases: []string{"auth", "signin"},
15 | Usage: "Login to your Openbin account.",
16 | Flags: []cli.Flag{
17 | &cli.StringFlag{
18 | Name: "email",
19 | Aliases: []string{"e"},
20 | Usage: "Your Openbin account email.",
21 | Required: false,
22 | },
23 | &cli.StringFlag{
24 | Name: "provider",
25 | Aliases: []string{"p"},
26 | Usage: "Your Openbin account provider.",
27 | Required: false,
28 | DefaultText: "github",
29 | },
30 | },
31 | Action: func(cCtx *cli.Context) error {
32 | codeVerifier := ""
33 | if cCtx.String("email") != "" {
34 | d, err := supabase.Auth.SignInWithOtp(ctx, sb.OtpSignInOptions{
35 | Email: cCtx.String("email"),
36 | RedirectTo: "http://localhost:8089/auth-callback",
37 | FlowType: sb.PKCE,
38 | })
39 |
40 | if err != nil {
41 | return err
42 | }
43 |
44 | codeVerifier = d.CodeVerifier
45 |
46 | fmt.Println("Check your email! 📧")
47 | } else {
48 | provider := cCtx.String("provider")
49 |
50 | if provider == "" {
51 | provider = "github"
52 | }
53 |
54 | d, err := supabase.Auth.SignInWithProvider(sb.ProviderSignInOptions{
55 | Provider: provider,
56 | RedirectTo: "http://localhost:8089/auth-callback",
57 | FlowType: sb.PKCE,
58 | })
59 |
60 | if err != nil {
61 | return err
62 | }
63 |
64 | codeVerifier = d.CodeVerifier
65 |
66 | fmt.Printf("Please go to the following URL to login: %s\n", d.URL)
67 | }
68 |
69 | stopServer := make(chan bool)
70 |
71 | // start a server to listen for the redirect
72 | //
73 | // 1. create a server
74 | // 2. listen for the redirect
75 | // 3. get the token
76 | // 4. save the token to the config file
77 | // 5. return a success message
78 | http.HandleFunc("/auth-callback", func(w http.ResponseWriter, r *http.Request) {
79 | // get the code
80 | code := r.URL.Query().Get("code")
81 | // fmt.Printf("Code: %s\n", code)
82 | // fmt.Printf("Code verifier: %s\n", codeVerifier)
83 | // get the token
84 | token, err := supabase.Auth.ExchangeCode(ctx, sb.ExchangeCodeOpts{
85 | AuthCode: code,
86 | CodeVerifier: codeVerifier,
87 | })
88 |
89 | if err != nil {
90 | fmt.Println(err)
91 | }
92 |
93 | // get expiry date
94 | expiryDate := time.Now().Add(time.Second * time.Duration(token.ExpiresIn))
95 |
96 | settings.SetAccessToken(token.AccessToken)
97 | settings.SetRefreshToken(token.RefreshToken)
98 | settings.SetTokenExpiry(expiryDate)
99 |
100 | fmt.Println("Successfully logged in! 🎉")
101 |
102 | // close the server
103 | w.Write([]byte("Successfully logged in! 🎉"))
104 |
105 | stopServer <- true
106 | })
107 |
108 | server := &http.Server{Addr: ":8089"}
109 |
110 | go func() {
111 | if err := server.ListenAndServe(); err != nil {
112 | fmt.Println(err)
113 | }
114 | }()
115 |
116 | <-stopServer
117 |
118 | if err := server.Shutdown(ctx); err != nil {
119 | fmt.Println(err)
120 | }
121 |
122 | return nil
123 | },
124 | }
125 |
126 | var LogoutCommand = cli.Command{
127 | Name: "logout",
128 | Aliases: []string{"signout"},
129 | Usage: "Logout from your Openbin account.",
130 | Action: func(cCtx *cli.Context) error {
131 | settings.SetAccessToken("")
132 | settings.SetRefreshToken("")
133 |
134 | fmt.Println("Successfully logged out! 👋")
135 |
136 | return nil
137 | },
138 | }
139 |
--------------------------------------------------------------------------------
/cli/cmd/delete.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | db "github.com/ethndotsh/openbin/cli/supabase"
7 | "github.com/urfave/cli/v2"
8 | )
9 |
10 | var DeleteCommand = cli.Command{
11 | Name: "delete",
12 | Aliases: []string{"del", "rm"},
13 | Usage: "Delete a paste.",
14 | ArgsUsage: `PASTE_ID`,
15 | Action: func(cCtx *cli.Context) error {
16 | pasteId := cCtx.Args().First()
17 |
18 | if pasteId == "" {
19 | cli.Exit("Please provide a paste ID.", 1)
20 | }
21 |
22 | user, err := supabase.Auth.User(ctx, settings.AccessToken)
23 |
24 | if err != nil {
25 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
26 | return err
27 | }
28 |
29 | supabase.DB.AddHeader("Authorization", fmt.Sprintf("Bearer %s", settings.AccessToken))
30 |
31 | var paste []Paste
32 |
33 | err = supabase.DB.From("pastes").Select("file").Eq("id", pasteId).Eq("author", user.ID).Execute(&paste)
34 |
35 | if err != nil {
36 | cli.Exit("Could not get the paste.", 1)
37 | return err
38 | }
39 |
40 | if len(paste) == 0 {
41 | cli.Exit("Could not find the paste.", 1)
42 | return err
43 | }
44 |
45 | err = supabase.DB.From("pastes").Delete().Eq("id", pasteId).Execute(nil)
46 |
47 | if err != nil {
48 | cli.Exit("Could not delete the paste.", 1)
49 | return err
50 | }
51 |
52 | sbAuth := db.NewAuth()
53 |
54 | sbAuth.Storage.From("pastes").Remove([]string{paste[0].File})
55 |
56 | fmt.Println("Deleted the paste! 🗑️")
57 |
58 | return nil
59 | },
60 | }
61 |
--------------------------------------------------------------------------------
/cli/cmd/pastes.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/urfave/cli/v2"
8 | )
9 |
10 | type CustomTime struct {
11 | time.Time
12 | }
13 |
14 | const customLayout = "2006-01-02T15:04:05.999999"
15 |
16 | // Implement the UnmarshalJSON method for CustomTime
17 | func (ct *CustomTime) UnmarshalJSON(data []byte) error {
18 | // Remove the surrounding quotes from the JSON string
19 | if string(data) == "null" {
20 | return nil
21 | }
22 | trimmedData := data[1 : len(data)-1]
23 | parsedTime, err := time.Parse(customLayout, string(trimmedData))
24 | if err != nil {
25 | return err
26 | }
27 | ct.Time = parsedTime
28 | return nil
29 | }
30 |
31 | type Paste struct {
32 | ID string `json:"id"`
33 | CreatedAt CustomTime `json:"created_at,omitempty"`
34 | Author string `json:"author"`
35 | File string `json:"file"`
36 | Draft bool `json:"draft"`
37 | ExpiresAt CustomTime `json:"expires_at,omitempty"`
38 | Title string `json:"title"`
39 | Description string `json:"description"`
40 | Language string `json:"language"`
41 | }
42 |
43 | var PastesCommand = cli.Command{
44 | Name: "pastes",
45 | Aliases: []string{"ls", "list"},
46 | Usage: "Manage your pastes.",
47 | Action: func(cCtx *cli.Context) error {
48 | user, err := supabase.Auth.User(ctx, settings.AccessToken)
49 |
50 | if err != nil {
51 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
52 | }
53 |
54 | if user == nil {
55 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
56 | }
57 |
58 | supabase.DB.AddHeader("Authorization", fmt.Sprintf("Bearer %s", settings.AccessToken))
59 |
60 | var pastes []Paste
61 | err = supabase.DB.From("pastes").Select("*").Eq("author", user.ID).Execute(&pastes)
62 |
63 | if err != nil {
64 | cli.Exit("Could not get the pastes.", 1)
65 | return err
66 | }
67 |
68 | for _, paste := range pastes {
69 | visibility := "Public"
70 |
71 | if paste.Draft {
72 | visibility = "Draft"
73 | }
74 |
75 | title := paste.Title
76 |
77 | if title == "" {
78 | title = "Untitled Paste"
79 | }
80 |
81 | fmt.Printf("\n- %s: %s [%s]\n", title, fmt.Sprintf("https://openbin.ethn.sh/paste/%s", paste.ID), visibility)
82 | }
83 |
84 | return nil
85 | },
86 | }
87 |
--------------------------------------------------------------------------------
/cli/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/ethndotsh/openbin/cli/config"
8 | db "github.com/ethndotsh/openbin/cli/supabase"
9 | )
10 |
11 | var ctx = context.Background()
12 | var supabase = db.New()
13 | var settings = config.New()
14 |
15 | func init() {
16 | now := time.Now()
17 |
18 | if settings.AccessToken != "" && settings.RefreshToken != "" {
19 | if settings.Expires.Before(now) {
20 | // refresh the token
21 | token, err := supabase.Auth.RefreshUser(ctx, settings.AccessToken, settings.RefreshToken)
22 |
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | // get expiry date
28 | expiryDate := time.Now().Add(time.Second * time.Duration(token.ExpiresIn))
29 |
30 | settings.SetAccessToken(token.AccessToken)
31 | settings.SetRefreshToken(token.RefreshToken)
32 | settings.SetTokenExpiry(expiryDate)
33 |
34 | supabase = db.New()
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/cli/cmd/upload.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | "time"
10 |
11 | "github.com/atotto/clipboard"
12 | uuid "github.com/doamatto/nobs-uuid"
13 | "github.com/ethndotsh/openbin/cli/config"
14 | db "github.com/ethndotsh/openbin/cli/supabase"
15 | "github.com/hairyhenderson/go-which"
16 | "github.com/pkg/browser"
17 | "github.com/urfave/cli/v2"
18 | )
19 |
20 | type UploadOptions struct {
21 | File string
22 | Editor string
23 | Draft bool
24 | Expire string
25 | Title string
26 | Description string
27 | Language string
28 | Copy bool
29 | Open bool
30 | Quiet bool
31 | }
32 |
33 | var UploadCommand = cli.Command{
34 | Name: "upload",
35 | Aliases: []string{"u", "up"},
36 | Usage: "Upload a file to Openbin.",
37 | ArgsUsage: "[FILE]",
38 | Flags: []cli.Flag{
39 | &cli.StringFlag{
40 | Name: "editor",
41 | Aliases: []string{"E"},
42 | Usage: "Set the editor to use to edit the paste. Must be the command executable (i.e. code, vim, nano, etc.)",
43 | Required: false,
44 | },
45 | &cli.BoolFlag{
46 | Name: "draft",
47 | Aliases: []string{"d"},
48 | Usage: "Set the paste to draft (private).",
49 | Required: false,
50 | },
51 | &cli.StringFlag{
52 | Name: "expire",
53 | Aliases: []string{"e"},
54 | Usage: "Set the paste to expire after a certain time.",
55 | Required: false,
56 | },
57 | &cli.StringFlag{
58 | Name: "title",
59 | Aliases: []string{"t"},
60 | Usage: "Set the paste title. Use quotes if it has spaces.",
61 | Required: false,
62 | },
63 | &cli.StringFlag{
64 | Name: "description",
65 | Usage: "Set the paste description. Use quotes if it has spaces.",
66 | Required: false,
67 | },
68 | &cli.StringFlag{
69 | Name: "language",
70 | Aliases: []string{"l"},
71 | Usage: "Set the paste language.",
72 | Required: false,
73 | },
74 | &cli.BoolFlag{
75 | Name: "copy",
76 | Aliases: []string{"c"},
77 | Usage: "Copy the paste URL to the clipboard.",
78 | Required: false,
79 | },
80 | &cli.BoolFlag{
81 | Name: "open",
82 | Aliases: []string{"o"},
83 | Usage: "Open the paste URL in the browser.",
84 | Required: false,
85 | },
86 | &cli.BoolFlag{
87 | Name: "quiet",
88 | Aliases: []string{"q"},
89 | Usage: "Don't print anything to the console. Errors will still be printed.",
90 | Required: false,
91 | },
92 | },
93 | Action: func(cCtx *cli.Context) error {
94 | user, err := supabase.Auth.User(ctx, settings.AccessToken)
95 |
96 | if err != nil {
97 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
98 | }
99 |
100 | if user == nil {
101 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
102 | }
103 |
104 | args := UploadOptions{
105 | File: cCtx.Args().First(),
106 | Editor: cCtx.String("editor"),
107 | Draft: cCtx.Bool("draft"),
108 | Expire: cCtx.String("expire"),
109 | Title: cCtx.String("title"),
110 | Description: cCtx.String("description"),
111 | Language: cCtx.String("language"),
112 | Copy: cCtx.Bool("copy"),
113 | Open: cCtx.Bool("open"),
114 | Quiet: cCtx.Bool("quiet"),
115 | }
116 |
117 | if args.File != "" {
118 | // Check if the file exists
119 | if _, err := os.Stat(args.File); os.IsNotExist(err) {
120 | return cli.Exit("The file you specified does not exist.", 1)
121 | }
122 | }
123 |
124 | if args.Language == "" {
125 | // Try to get the language from the file extension
126 | if args.File != "" {
127 | fileExt := config.GetFileExtension(args.File)
128 | if config.IsFileTypeAllowed(fileExt) {
129 | filetype := config.GetFileTypeByFilePath(args.File)
130 | args.Language = filetype.Value
131 | } else {
132 | return cli.Exit(fmt.Sprintf("The file type you specified is not allowed.\nYou specified: %s\nAllowed types: %s", config.GetFileExtension(args.File), strings.Join(config.GetAllowedTypes(), ", ")), 1)
133 | }
134 | }
135 | } else {
136 | // Check if the language is allowed
137 | if !config.IsFileTypeAllowedByValue(args.Language) {
138 | return cli.Exit(fmt.Sprintf("The file type you specified is not allowed.\nYou specified: %s\nAllowed types: %s", config.GetFileExtension(args.Language), strings.Join(config.GetAllowedTypes(), ", ")), 1)
139 | }
140 | }
141 |
142 | if args.Editor != "" {
143 | editorPath := which.Which(args.Editor)
144 | if editorPath == "" {
145 | return cli.Exit("The editor you specified could not be found.", 1)
146 | }
147 |
148 | path := ""
149 |
150 | if args.File == "" {
151 | // make a temporary file
152 | // open the editor
153 |
154 | f, err := ioutil.TempFile("", "openbin-*.txt")
155 |
156 | if err != nil {
157 | return cli.Exit("Could not create a temporary file.", 1)
158 | }
159 |
160 | path = f.Name()
161 | } else {
162 | path = args.File
163 | }
164 |
165 | if !args.Quiet {
166 | fmt.Println("Waiting for you to finish editing...")
167 | }
168 |
169 | if args.Editor == "code" {
170 | err := exec.Command(editorPath, "-r", "--wait", path).Run()
171 |
172 | if err != nil {
173 | return cli.Exit("Could not open the editor.", 1)
174 | }
175 | } else {
176 | err := exec.Command(editorPath, path).Run()
177 |
178 | if err != nil {
179 | return cli.Exit("Could not open the editor.", 1)
180 | }
181 | }
182 |
183 | // upload the file
184 | UploadFile(path, args)
185 |
186 | // delete the temporary file
187 | if args.File == "" {
188 | os.Remove(path)
189 | }
190 |
191 | if !args.Quiet {
192 | fmt.Println("Uploaded the file! 🎉")
193 | }
194 | return nil
195 | }
196 |
197 | if args.File == "" && args.Editor == "" {
198 | return cli.Exit("You must specify a file or an editor.", 1)
199 | }
200 |
201 | if args.File != "" {
202 | UploadFile(args.File, args)
203 | if !args.Quiet {
204 | fmt.Println("Uploaded the file! 🎉")
205 | }
206 | }
207 |
208 | return nil
209 | },
210 | }
211 |
212 | func UploadFile(path string, opts UploadOptions) {
213 | _, id, err := uuid.GenUUID()
214 |
215 | if err != nil {
216 | cli.Exit("Could not generate a UUID.", 1)
217 | }
218 |
219 | data, err := os.Open(path)
220 |
221 | if err != nil {
222 | cli.Exit("Could not open the file.", 1)
223 | }
224 |
225 | sbAuth := db.NewAuth()
226 |
227 | sbAuth.Storage.From("pastes").Upload(fmt.Sprintf("pastes/openbin-%s.txt", id), data)
228 |
229 | // expires_at should be a UTC string from this format: "MM-dd-yyyy hh:mm"
230 |
231 | var expires_at *time.Time
232 |
233 | if opts.Expire != "" {
234 | e, err := time.Parse(opts.Expire, "01-02-2006 03:04")
235 |
236 | if err != nil {
237 | cli.Exit("Could not parse the expiration date.", 1)
238 | }
239 |
240 | expires_at = &e
241 | }
242 |
243 | user, err := supabase.Auth.User(ctx, settings.AccessToken)
244 |
245 | if err != nil {
246 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
247 | }
248 |
249 | if user == nil {
250 | cli.Exit("You don't seem to be signed in. Try running `openbin login` to sign in.", 1)
251 | }
252 |
253 | supabase.DB.AddHeader("Authorization", fmt.Sprintf("Bearer %s", settings.AccessToken))
254 |
255 | err = supabase.DB.From("pastes").Insert(map[string]interface{}{
256 | "id": id,
257 | "title": opts.Title,
258 | "description": opts.Description,
259 | "language": opts.Language,
260 | "draft": opts.Draft,
261 | "expires_at": expires_at,
262 | "file": fmt.Sprintf("pastes/openbin-%s.txt", id),
263 | "author": user.ID,
264 | }).ExecuteWithContext(ctx, nil)
265 |
266 | if err != nil {
267 | cli.Exit("Could not upload the file.", 1)
268 | }
269 |
270 | if !opts.Quiet {
271 | fmt.Printf("https://openbin.ethn.sh/pastes/%s\n", id)
272 | }
273 |
274 | if opts.Copy {
275 | // copy the URL to the clipboard
276 | err = clipboard.WriteAll(fmt.Sprintf("https://openbin.ethn.sh/pastes/%s", id))
277 |
278 | if err != nil {
279 | fmt.Println(err)
280 | cli.Exit("Could not copy the URL to the clipboard.", 1)
281 | }
282 | }
283 |
284 | if opts.Open {
285 | // open the URL in the browser
286 | err = browser.OpenURL(fmt.Sprintf("https://openbin.ethn.sh/pastes/%s", id))
287 |
288 | if err != nil {
289 | fmt.Println(err)
290 | cli.Exit("Could not open the URL in the browser.", 1)
291 | }
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/cli/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/kirsle/configdir"
10 | )
11 |
12 | type AppSettings struct {
13 | AccessToken string
14 | RefreshToken string
15 | Expires time.Time
16 | }
17 |
18 | func New() AppSettings {
19 | configPath := configdir.LocalConfig("openbin")
20 |
21 | // create the config file if it doesn't exist
22 | if err := configdir.MakePath(configPath); err != nil {
23 | panic(err)
24 | }
25 |
26 | configFile := filepath.Join(configPath, "settings.json")
27 |
28 | var settings AppSettings
29 |
30 | // create the config file if it doesn't exist
31 | if _, err := os.Stat(configFile); os.IsNotExist(err) {
32 | // Create the new config file.
33 | settings = AppSettings{}
34 | fh, err := os.Create(configFile)
35 | if err != nil {
36 | panic(err)
37 | }
38 | defer fh.Close()
39 |
40 | encoder := json.NewEncoder(fh)
41 | encoder.Encode(&settings)
42 | } else {
43 | // Load the existing file.
44 | fh, err := os.Open(configFile)
45 | if err != nil {
46 | panic(err)
47 | }
48 | defer fh.Close()
49 |
50 | decoder := json.NewDecoder(fh)
51 | decoder.Decode(&settings)
52 | }
53 |
54 | return settings
55 | }
56 |
57 | func (s *AppSettings) Save() {
58 | configPath := configdir.LocalConfig("openbin")
59 | configFile := filepath.Join(configPath, "settings.json")
60 |
61 | fh, err := os.OpenFile(configFile, os.O_WRONLY|os.O_TRUNC, 0644)
62 | if err != nil {
63 | panic(err)
64 | }
65 | defer fh.Close()
66 |
67 | encoder := json.NewEncoder(fh)
68 | encoder.Encode(s)
69 | }
70 |
71 | func (s *AppSettings) SetAccessToken(token string) {
72 | s.AccessToken = token
73 | s.Save()
74 | }
75 |
76 | func (s *AppSettings) SetRefreshToken(token string) {
77 | s.RefreshToken = token
78 | s.Save()
79 | }
80 |
81 | func (s *AppSettings) GetAccessToken() string {
82 | return s.AccessToken
83 | }
84 |
85 | func (s *AppSettings) GetRefreshToken() string {
86 | return s.RefreshToken
87 | }
88 |
89 | func (s *AppSettings) SetTokenExpiry(expiry time.Time) {
90 | s.Expires = expiry
91 | s.Save()
92 | }
93 |
94 | func (s *AppSettings) GetTokenExpiry() time.Time {
95 | return s.Expires
96 | }
97 |
--------------------------------------------------------------------------------
/cli/config/fileTypes.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 | )
7 |
8 | type FileType struct {
9 | Name string `json:"name"`
10 | Value string `json:"value"`
11 | Extensions []string `json:"extensions"`
12 | }
13 |
14 | var AcceptedTypes = []FileType{
15 | {
16 | Name: "Plain Text",
17 | Value: "plaintext",
18 | Extensions: []string{"txt"},
19 | },
20 | {
21 | Name: "JavaScript",
22 | Value: "javascript",
23 | Extensions: []string{"js", "jsx", "mjs", "cjs"},
24 | },
25 | {
26 | Name: "TypeScript",
27 | Value: "typescript",
28 | Extensions: []string{"ts", "mts", "cts", "tsx"},
29 | },
30 | {
31 | Name: "Python",
32 | Value: "python",
33 | Extensions: []string{"py"},
34 | },
35 | {
36 | Name: "JSON",
37 | Value: "json",
38 | Extensions: []string{"json"},
39 | },
40 | {
41 | Name: "HTML",
42 | Value: "html",
43 | Extensions: []string{"html"},
44 | },
45 | {
46 | Name: "CSS",
47 | Value: "css",
48 | Extensions: []string{"css"},
49 | },
50 | {
51 | Name: "Markdown",
52 | Value: "markdown",
53 | Extensions: []string{"md"},
54 | },
55 | {
56 | Name: "Lua",
57 | Value: "lua",
58 | Extensions: []string{"lua"},
59 | },
60 | {
61 | Name: "Go",
62 | Value: "go",
63 | Extensions: []string{"go"},
64 | },
65 | {
66 | Name: "Rust",
67 | Value: "rust",
68 | Extensions: []string{"rs"},
69 | },
70 | {
71 | Name: "XML",
72 | Value: "xml",
73 | Extensions: []string{"xml"},
74 | },
75 | {
76 | Name: "YAML",
77 | Value: "yaml",
78 | Extensions: []string{"yaml", "yml"},
79 | },
80 | {
81 | Name: "MDX",
82 | Value: "mdx",
83 | Extensions: []string{"mdx"},
84 | },
85 | {
86 | Name: "SCSS",
87 | Value: "scss",
88 | Extensions: []string{"scss"},
89 | },
90 | {
91 | Name: "Less",
92 | Value: "less",
93 | Extensions: []string{"less"},
94 | },
95 | {
96 | Name: "Elixir",
97 | Value: "elixir",
98 | Extensions: []string{"ex", "exs"},
99 | },
100 | {
101 | Name: "C++",
102 | Value: "cpp",
103 | Extensions: []string{"cpp"},
104 | },
105 | {
106 | Name: "C#",
107 | Value: "csharp",
108 | Extensions: []string{"cs"},
109 | },
110 | {
111 | Name: "Java",
112 | Value: "java",
113 | Extensions: []string{"java"},
114 | },
115 | {
116 | Name: "Kotlin",
117 | Value: "kotlin",
118 | Extensions: []string{"kt"},
119 | },
120 | {
121 | Name: "Dart",
122 | Value: "dart",
123 | Extensions: []string{"dart"},
124 | },
125 | {
126 | Name: "Scala",
127 | Value: "scala",
128 | Extensions: []string{"scala"},
129 | },
130 | {
131 | Name: "Handlebars",
132 | Value: "handlebars",
133 | Extensions: []string{"hbs", "handlebars"},
134 | },
135 | {
136 | Name: "Pug",
137 | Value: "pug",
138 | Extensions: []string{"pug"},
139 | },
140 | {
141 | Name: "Ruby",
142 | Value: "ruby",
143 | Extensions: []string{"rb"},
144 | },
145 | {
146 | Name: "PHP",
147 | Value: "php",
148 | Extensions: []string{"php"},
149 | },
150 | {
151 | Name: "Swift",
152 | Value: "swift",
153 | Extensions: []string{"swift"},
154 | },
155 | {
156 | Name: "Solidity",
157 | Value: "solidity",
158 | Extensions: []string{"sol"},
159 | },
160 | {
161 | Name: "SQL",
162 | Value: "sql",
163 | Extensions: []string{"sql"},
164 | },
165 | {
166 | Name: "Dockerfile",
167 | Value: "dockerfile",
168 | Extensions: []string{"dockerfile"},
169 | },
170 | {
171 | Name: "Shell",
172 | Value: "shell",
173 | Extensions: []string{"sh"},
174 | },
175 | {
176 | Name: "PowerShell",
177 | Value: "powershell",
178 | Extensions: []string{"ps1"},
179 | },
180 | {
181 | Name: "R",
182 | Value: "r",
183 | Extensions: []string{"r"},
184 | },
185 | {
186 | Name: "Perl",
187 | Value: "perl",
188 | Extensions: []string{"pl"},
189 | },
190 | {
191 | Name: "GraphQL",
192 | Value: "graphql",
193 | Extensions: []string{"graphql", "gql"},
194 | },
195 | {
196 | Name: "Clojure",
197 | Value: "clojure",
198 | Extensions: []string{"clj"},
199 | },
200 | {
201 | Name: "Objective-C",
202 | Value: "objective-c",
203 | Extensions: []string{"m"},
204 | },
205 | }
206 |
207 | func GetFileTypeByExtension(extension string) FileType {
208 | for _, acceptedType := range AcceptedTypes {
209 | for _, ext := range acceptedType.Extensions {
210 | if ext == extension {
211 | return acceptedType
212 | }
213 | }
214 | }
215 | return FileType{}
216 | }
217 |
218 | func GetFileTypeByValue(value string) FileType {
219 | for _, acceptedType := range AcceptedTypes {
220 | if acceptedType.Value == value {
221 | return acceptedType
222 | }
223 | }
224 | return FileType{}
225 | }
226 |
227 | func GetFileTypeByName(name string) FileType {
228 | for _, acceptedType := range AcceptedTypes {
229 | if acceptedType.Name == name {
230 | return acceptedType
231 | }
232 | }
233 | return FileType{}
234 | }
235 |
236 | func GetFileTypeByFilePath(filePath string) FileType {
237 | extension := GetFileExtension(filePath)
238 | return GetFileTypeByExtension(extension)
239 | }
240 |
241 | func GetFileExtension(filePath string) string {
242 | // Get the file extension
243 | extension := filepath.Ext(filePath)
244 | // Remove the dot
245 | extension = strings.Replace(extension, ".", "", -1)
246 | return extension
247 | }
248 |
249 | func IsFileTypeAllowed(ext string) bool {
250 | for _, acceptedType := range AcceptedTypes {
251 | for _, extension := range acceptedType.Extensions {
252 | if extension == ext {
253 | return true
254 | }
255 | }
256 | }
257 | return false
258 | }
259 |
260 | func IsFileTypeAllowedByValue(value string) bool {
261 | for _, acceptedType := range AcceptedTypes {
262 | if acceptedType.Value == value {
263 | return true
264 | }
265 | }
266 | return false
267 | }
268 |
269 | func GetAllowedTypes() []string {
270 | var allowedTypes []string
271 | for _, acceptedType := range AcceptedTypes {
272 | allowedTypes = append(allowedTypes, acceptedType.Value)
273 | }
274 | return allowedTypes
275 | }
276 |
--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "os"
10 | "strings"
11 |
12 | "github.com/ethndotsh/openbin/cli/cmd"
13 | "github.com/urfave/cli/v2"
14 | )
15 |
16 | const VERSION = "1.0.8"
17 |
18 | type GitHubResponse struct {
19 | TagName string `json:"tag_name"`
20 | }
21 |
22 | func Run() {
23 | app := &cli.App{
24 | Name: "openbin",
25 | HelpName: "openbin",
26 | EnableBashCompletion: true,
27 | Description: "A CLI tool for Openbin, a free and open-source pastebin alternative built primarily for command-line warriors.",
28 | Flags: []cli.Flag{
29 | &cli.BoolFlag{
30 | Name: "version",
31 | Aliases: []string{
32 | "v",
33 | },
34 | Usage: "Print the version of openbin.",
35 | },
36 | },
37 | Action: func(cCtx *cli.Context) error {
38 | if cCtx.Bool("version") {
39 | fmt.Println("openbin version " + VERSION)
40 | return nil
41 | }
42 | fmt.Println("Welcome to openbin! Run `openbin help` to get started.")
43 | return nil
44 | },
45 | After: func(cCtx *cli.Context) error {
46 | // Check for updates, and if there are any, notify the user.
47 | res, err := http.Get("https://api.github.com/repos/ethndotsh/openbin/releases/latest")
48 | if err != nil {
49 | // If there's an error, just return.
50 | return nil
51 | }
52 |
53 | if res.StatusCode != 200 {
54 | // If there's an error, just return.
55 | return nil
56 | }
57 |
58 | // Get the "tag_name" field from the JSON response.
59 | // This is the latest version of openbin.
60 | // If there is a newer version, notify the user.
61 |
62 | var response GitHubResponse
63 |
64 | defer res.Body.Close()
65 |
66 | body, err := ioutil.ReadAll(res.Body)
67 |
68 | if err != nil {
69 | return nil
70 | }
71 |
72 | err = json.Unmarshal(body, &response)
73 |
74 | if err != nil {
75 | return nil
76 | }
77 |
78 | if compareVersions(VERSION, response.TagName) == -1 {
79 | fmt.Printf("There is a new version of openbin available! You are running %s, and the latest version is %s.\n", VERSION, response.TagName)
80 | fmt.Println("You can update by running whatever command you used to install openbin.")
81 | }
82 |
83 | return nil
84 | },
85 | Commands: []*cli.Command{
86 | &cmd.LoginCommand,
87 | &cmd.LogoutCommand,
88 | &cmd.UploadCommand,
89 | &cmd.PastesCommand,
90 | &cmd.DeleteCommand,
91 | },
92 | }
93 |
94 | if err := app.Run(os.Args); err != nil {
95 | log.Fatal(err)
96 | }
97 | }
98 |
99 | func compareVersions(a string, b string) int {
100 | // check the major, minor, and patch versions
101 | // if a > b, return 1
102 | // if a < b, return -1
103 | // if a == b, return 0
104 |
105 | // split the versions into arrays
106 | aArr := strings.Split(a, ".")
107 | bArr := strings.Split(b, ".")
108 |
109 | // compare the major versions
110 | if aArr[0] > bArr[0] {
111 | return 1
112 | } else if aArr[0] < bArr[0] {
113 | return -1
114 | }
115 |
116 | // compare the minor versions
117 | if aArr[1] > bArr[1] {
118 | return 1
119 | } else if aArr[1] < bArr[1] {
120 | return -1
121 | }
122 |
123 | // compare the patch versions
124 | if aArr[2] > bArr[2] {
125 | return 1
126 | } else if aArr[2] < bArr[2] {
127 | return -1
128 | }
129 |
130 | return 0
131 | }
132 |
--------------------------------------------------------------------------------
/cli/supabase/supabase.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/ethndotsh/openbin/cli/config"
5 | "github.com/jackmerrill/supabase-go"
6 | )
7 |
8 | const SUPABASE_URL = "https://yjcmvygieeqeptqmiykg.supabase.co"
9 | const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlqY212eWdpZWVxZXB0cW1peWtnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDI0NDE0NjgsImV4cCI6MjA1ODAxNzQ2OH0.9vyMbaYpYKrXC9WlTrEuFdyVglhaNxoUV4t9d9mfgJA"
10 |
11 | func New() *supabase.Client {
12 | supabaseClient := supabase.CreateClient(SUPABASE_URL, SUPABASE_KEY)
13 | return supabaseClient
14 | }
15 |
16 | func NewAuth() *supabase.Client {
17 | settings := config.New()
18 |
19 | if settings.AccessToken == "" {
20 | panic("No access token found. Please login with `openbin login`.")
21 | }
22 |
23 | return supabase.CreateClient(SUPABASE_URL, settings.AccessToken)
24 | }
25 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ethndotsh/openbin
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/atotto/clipboard v0.1.4
7 | github.com/doamatto/nobs-uuid v0.0.0-20230404013526-6ced46a9f4e8
8 | github.com/hairyhenderson/go-which v0.2.0
9 | github.com/jackmerrill/supabase-go v0.4.1
10 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
11 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
12 | github.com/urfave/cli/v2 v2.25.7
13 | )
14 |
15 | require (
16 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
17 | github.com/google/go-querystring v1.1.0 // indirect
18 | github.com/nedpals/postgrest-go v0.1.3 // indirect
19 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
20 | github.com/spf13/afero v1.3.3 // indirect
21 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
22 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 // indirect
23 | golang.org/x/text v0.3.0 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
4 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/doamatto/nobs-uuid v0.0.0-20230404013526-6ced46a9f4e8 h1:MhLhwnEFV83k54BhlPLEAo2xUrWotX3O8xFPyThIlvI=
7 | github.com/doamatto/nobs-uuid v0.0.0-20230404013526-6ced46a9f4e8/go.mod h1:M4rKZKBTsXjSw7DpqJ+G469ALT5mdj36tkmGG9UyKpI=
8 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
9 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
10 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
11 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
12 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
13 | github.com/hairyhenderson/go-which v0.2.0 h1:vxoCKdgYc6+MTBzkJYhWegksHjjxuXPNiqo5G2oBM+4=
14 | github.com/hairyhenderson/go-which v0.2.0/go.mod h1:U1BQQRCjxYHfOkXDyCgst7OZVknbqI7KuGKhGnmyIik=
15 | github.com/jackmerrill/supabase-go v0.4.1 h1:jbsbeFqnMOMiYvFd5tL7XSJvzxXqhRbBQ4FR6NEux00=
16 | github.com/jackmerrill/supabase-go v0.4.1/go.mod h1:vSyl/8/hCBcqmwv8t9bow9sDWR3CVnCnVPEUnKL2Q08=
17 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
18 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
19 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
20 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
21 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
22 | github.com/nedpals/postgrest-go v0.1.3 h1:ZC3aPPx9rDTWQWzvnWI60lJWjAqgCCD/U6hcHp3NL0w=
23 | github.com/nedpals/postgrest-go v0.1.3/go.mod h1:RGinB2OXsnGLcZMu5avS0U+b9npyZmk+ecK74UDi/xY=
24 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
25 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
26 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
27 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
28 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
29 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
32 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
33 | github.com/spf13/afero v1.3.3 h1:p5gZEKLYoL7wh8VrJesMaYeNxdEd1v3cb4irOk9zB54=
34 | github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
35 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
37 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
38 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
39 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
40 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
41 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
43 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
44 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
49 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk=
50 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
51 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
52 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
53 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
54 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
56 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
58 | gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
59 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
60 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env pwsh
2 | # Inspired from the Deno install script: https://deno.land/x/install@v0.1.8/install.ps1?source=
3 |
4 | $ErrorActionPreference = "Stop"
5 |
6 | $BinDir = "${Home}\.openbin\bin"
7 |
8 | $DownloadUrl = "https://github.com/ethndotsh/openbin/releases/latest/download/openbin-windows-amd64.exe"
9 |
10 | $DownloadPath = "${BinDir}\openbin.exe"
11 |
12 | if (!(Test-Path $BinDir)) {
13 | New-Item -ItemType Directory -Force -Path $BinDir
14 | }
15 |
16 | curl.exe -Lo $DownloadPath $DownloadUrl
17 |
18 | $User = [System.EnvironmentVariableTarget]::User
19 | $Path = [System.Environment]::GetEnvironmentVariable("Path", $User)
20 | if (!(";${Path};".ToLower() -like "*;${BinDir}:*".ToLower())) {
21 | [System.Environment]::SetEnvironmentVariable("Path", "${Path};${BinDir}", $User)
22 | $Env:Path += ";${BinDir}"
23 | }
24 |
25 | # check for the "NoAlias" flag, if it's set, we'll skip aliasing. Alias to `ob`
26 | if ($args[0] -ne "--no-alias") {
27 | $Alias = "ob"
28 | if (Test-Path alias:\$Alias) {
29 | # if the alias already exists, don't remove it, but warn the user
30 | Write-Host "⚠️ The alias '$Alias' is already in your PowerShell profile. We won't change it."
31 | } else {
32 | # if the alias doesn't exist, add it
33 | New-Item -Path $PROFILE.CurrentUserAllHosts -ItemType File -Force
34 | Add-Content -Path $PROFILE.CurrentUserAllHosts -Value "Set-Alias -Name $Alias -Value $DownloadPath"
35 | Write-Host "Alias '$Alias' created!"
36 | }
37 | }
38 |
39 | Write-Host "🎉 Openbin Installed!"
40 | Write-Host ""
41 | Write-Host "Run 'openbin --help' to get started"
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Inspired from the Deno install script: https://deno.land/x/install@v0.1.8/install.sh?source=
3 |
4 | set -e
5 |
6 | if [ "$OS" = "Windows_NT" ]; then
7 | target="windows"
8 | ext=".exe"
9 | case $(uname -m) in
10 | x86_64) arch="amd64" ;;
11 | aarch64) arch="arm64" ;;
12 | *)
13 | echo "Unsupported architecture: $(uname -m)"
14 | exit 1
15 | ;;
16 | esac
17 | else
18 | ext=""
19 | case $(uname -sm) in
20 | "Darwin x86_64")
21 | target="darwin"
22 | arch="amd64"
23 | ;;
24 | "Darwin arm64")
25 | target="darwin"
26 | arch="arm64"
27 | ;;
28 | "Linux x86_64")
29 | target="linux"
30 | arch="amd64"
31 | ;;
32 | "Linux armv8")
33 | target="linux"
34 | arch="arm64"
35 | ;;
36 | *)
37 | echo "Unsupported platform: $(uname -sm)"
38 | exit 1
39 | ;;
40 | esac
41 | fi
42 |
43 | openbin_uri="https://github.com/ethndotsh/openbin/releases/latest/download/openbin-${target}-${arch}${ext}"
44 |
45 | bin_dir="${HOME}/.openbin/bin"
46 |
47 | if [ ! -d "$bin_dir" ]; then
48 | mkdir -p "$bin_dir"
49 | fi
50 |
51 | echo "Downloading binary..."
52 |
53 | curl --fail --location --progress-bar --output "$bin_dir/openbin${ext}" "$openbin_uri"
54 | chmod +x "$bin_dir/openbin${ext}"
55 |
56 | echo "Installing binary..."
57 |
58 | if command -v openbin >/dev/null; then
59 | echo "Run 'openbin --help' to get started"
60 | else
61 | case $SHELL in
62 | /bin/zsh) shell_profile=".zshrc" ;;
63 | *) shell_profile=".bashrc" ;;
64 | esac
65 | echo "Adding openbin to $HOME/$shell_profile"
66 | echo "" >>"$HOME/$shell_profile"
67 | echo "# openbin" >>"$HOME/$shell_profile"
68 | echo "export PATH=\"$bin_dir:\$PATH\"" >>"$HOME/$shell_profile"
69 | fi
70 |
71 | # check for the "--no-alias" flag, if not present, add alias (`ob`)
72 | if [ "$1" != "--no-alias" ]; then
73 | case $SHELL in
74 | /bin/zsh) shell_profile=".zshrc" ;;
75 | *) shell_profile=".bashrc" ;;
76 | esac
77 | echo "Adding openbin alias to $HOME/$shell_profile"
78 | echo "" >>"$HOME/$shell_profile"
79 | echo "# openbin alias" >>"$HOME/$shell_profile"
80 | echo "alias ob=openbin" >>"$HOME/$shell_profile"
81 | fi
82 |
83 | echo "🎉 Openbin Installed!"
84 | echo ""
85 | echo "Restart your shell or update your PATH:"
86 | echo ""
87 | echo " export PATH=\"$bin_dir:\$PATH\""
88 | echo ""
89 | echo "Run 'openbin --help' to get started"
90 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ethndotsh/openbin/cli"
5 | )
6 |
7 | func main() {
8 | cli.Run()
9 | }
10 |
--------------------------------------------------------------------------------
/openbin.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "project-root",
5 | "path": "./"
6 | },
7 | {
8 | "name": "supabase-functions",
9 | "path": "supabase/functions"
10 | }
11 | ],
12 | "settings": {
13 | "files.exclude": {
14 | "supabase/functions/": true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openbin",
3 | "version": "1.0.7",
4 | "description": "Pastebin clone that takes notes & code sharing to the next level.",
5 | "scripts": {
6 | "install": "go install github.com/ethndotsh/openbin@latest",
7 | "uninstall": "rm -rf $GOPATH/bin/openbin"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/ethndotsh/openbin.git"
12 | },
13 | "keywords": [
14 | "pastebin",
15 | "openbin",
16 | "supabase"
17 | ],
18 | "author": "The Openbin Team",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/ethndotsh/openbin/issues"
22 | },
23 | "homepage": "https://github.com/ethndotsh/openbin#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .: {}
10 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
2 | # working directory name when running `supabase init`.
3 | project_id = "openbin"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # Port used by db diff command to initialise the shadow database.
21 | shadow_port = 54320
22 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
23 | # server_version;` on the remote database to check.
24 | major_version = 15
25 |
26 | [studio]
27 | enabled = true
28 | # Port to use for Supabase Studio.
29 | port = 54323
30 | # External URL of the API server that frontend connects to.
31 | api_url = "http://localhost"
32 |
33 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
34 | # are monitored, and you can view the emails that would have been sent from the web interface.
35 | [inbucket]
36 | enabled = true
37 | # Port to use for the email testing server web interface.
38 | port = 54324
39 | # Uncomment to expose additional ports for testing user applications that send emails.
40 | # smtp_port = 54325
41 | # pop3_port = 54326
42 |
43 | [storage]
44 | # The maximum file size allowed (e.g. "5MB", "500KB").
45 | file_size_limit = "50MiB"
46 |
47 | [auth]
48 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
49 | # in emails.
50 | site_url = "http://localhost:3000"
51 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
52 | additional_redirect_urls = ["https://localhost:3000"]
53 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
54 | jwt_expiry = 3600
55 | # If disabled, the refresh token will never expire.
56 | enable_refresh_token_rotation = true
57 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
58 | # Requires enable_refresh_token_rotation = true.
59 | refresh_token_reuse_interval = 10
60 | # Allow/disallow new user signups to your project.
61 | enable_signup = true
62 |
63 | [auth.email]
64 | # Allow/disallow new user signups via email to your project.
65 | enable_signup = true
66 | # If enabled, a user will be required to confirm any email change on both the old, and new email
67 | # addresses. If disabled, only the new email is required to confirm.
68 | double_confirm_changes = true
69 | # If enabled, users need to confirm their email address before signing in.
70 | enable_confirmations = false
71 |
72 | [auth.sms]
73 | # Allow/disallow new user signups via SMS to your project.
74 | enable_signup = true
75 | # If enabled, users need to confirm their phone number before signing in.
76 | enable_confirmations = false
77 |
78 | # Configure one of the supported SMS providers: `twilio`, `messagebird`, `textlocal`, `vonage`.
79 | [auth.sms.twilio]
80 | enabled = false
81 | account_sid = ""
82 | message_service_sid = ""
83 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
84 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
85 |
86 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
87 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
88 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
89 | [auth.external.apple]
90 | enabled = false
91 | client_id = ""
92 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
93 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
94 | # Overrides the default auth redirectUrl.
95 | redirect_uri = ""
96 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
97 | # or any other third-party OIDC providers.
98 | url = ""
99 |
100 | [analytics]
101 | enabled = false
102 | port = 54327
103 | vector_port = 54328
104 | # Setup BigQuery project to enable log viewer on local development stack.
105 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging
106 | gcp_project_id = ""
107 | gcp_project_number = ""
108 | gcp_jwt_path = "supabase/gcloud.json"
109 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethndotsh/openbin/3d61b6e5ec26e82da539338d17271ed1ed589d7c/supabase/seed.sql
--------------------------------------------------------------------------------
/supabase/types.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json | undefined }
7 | | Json[]
8 |
9 | export interface Database {
10 | public: {
11 | Tables: {
12 | pastes: {
13 | Row: {
14 | author: string | null
15 | created_at: string | null
16 | description: string | null
17 | expires_at: string | null
18 | file: string | null
19 | id: string
20 | private: boolean | null
21 | syntax: string | null
22 | title: string | null
23 | }
24 | Insert: {
25 | author?: string | null
26 | created_at?: string | null
27 | description?: string | null
28 | expires_at?: string | null
29 | file?: string | null
30 | id?: string
31 | private?: boolean | null
32 | syntax?: string | null
33 | title?: string | null
34 | }
35 | Update: {
36 | author?: string | null
37 | created_at?: string | null
38 | description?: string | null
39 | expires_at?: string | null
40 | file?: string | null
41 | id?: string
42 | private?: boolean | null
43 | syntax?: string | null
44 | title?: string | null
45 | }
46 | Relationships: [
47 | {
48 | foreignKeyName: "pastes_author_fkey"
49 | columns: ["author"]
50 | referencedRelation: "users"
51 | referencedColumns: ["id"]
52 | }
53 | ]
54 | }
55 | }
56 | Views: {
57 | [_ in never]: never
58 | }
59 | Functions: {
60 | [_ in never]: never
61 | }
62 | Enums: {
63 | [_ in never]: never
64 | }
65 | CompositeTypes: {
66 | [_ in never]: never
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | # i'll .gitignore your mom
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
18 |
19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/web/_eslintrc.cjs:
--------------------------------------------------------------------------------
1 | // (e)slint-disable-next-line @typescript-eslint/no-var-requires
2 | const path = require("path");
3 |
4 | /** @type {import("eslint").Linter.Config} */
5 | const config = {
6 | overrides: [
7 | {
8 | extends: [
9 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
10 | ],
11 | files: ["*.ts", "*.tsx"],
12 | parserOptions: {
13 | project: path.join(__dirname, "tsconfig.json"),
14 | },
15 | },
16 | ],
17 | parser: "@typescript-eslint/parser",
18 | parserOptions: {
19 | project: path.join(__dirname, "tsconfig.json"),
20 | },
21 | plugins: ["@typescript-eslint"],
22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
23 | rules: {
24 | "@typescript-eslint/consistent-type-imports": [
25 | "warn",
26 | {
27 | prefer: "type-imports",
28 | fixStyle: "inline-type-imports",
29 | },
30 | ],
31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
32 | },
33 | };
34 |
35 | module.exports = config;
36 |
--------------------------------------------------------------------------------
/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/utils/cn"
15 | }
16 | }
--------------------------------------------------------------------------------
/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverActions: true,
5 | },
6 | images: {
7 | domains: ["avatars.githubusercontent.com", "api.dicebear.com"],
8 | },
9 | };
10 |
11 | module.exports = nextConfig;
12 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.2.0",
13 | "@monaco-editor/react": "^4.5.1",
14 | "@radix-ui/react-checkbox": "^1.0.4",
15 | "@radix-ui/react-dialog": "^1.0.4",
16 | "@radix-ui/react-dropdown-menu": "^2.0.5",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-popover": "^1.0.6",
19 | "@radix-ui/react-separator": "^1.0.3",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "@radix-ui/react-tooltip": "^1.0.6",
22 | "@supabase/auth-helpers-nextjs": "^0.7.4",
23 | "@supabase/supabase-js": "^2.32.0",
24 | "class-variance-authority": "^0.7.0",
25 | "clsx": "^2.0.0",
26 | "cmdk": "^0.2.0",
27 | "date-fns": "^2.30.0",
28 | "lucide-react": "^0.263.1",
29 | "monaco-editor": "^0.41.0",
30 | "murmurhash2": "^0.1.0",
31 | "next": "13.4.13",
32 | "react": "18.2.0",
33 | "react-day-picker": "^8.8.0",
34 | "react-dom": "18.2.0",
35 | "react-hook-form": "^7.45.4",
36 | "react-hotkeys-hook": "^4.4.1",
37 | "react-wrap-balancer": "^1.0.0",
38 | "sonner": "^0.6.2",
39 | "supabase": ">=1.8.1",
40 | "tailwind-merge": "^1.14.0",
41 | "tailwindcss-animate": "^1.0.6",
42 | "tinycolor2": "^1.6.0",
43 | "uuid": "^9.0.0",
44 | "zact": "^0.0.2",
45 | "zod": "^3.21.4"
46 | },
47 | "devDependencies": {
48 | "@tailwindcss/typography": "^0.5.9",
49 | "@types/eslint": "^8.44.2",
50 | "@types/node": "20.4.8",
51 | "@types/react": "18.2.18",
52 | "@types/react-dom": "18.2.7",
53 | "@types/tinycolor2": "^1.4.3",
54 | "@types/uuid": "^9.0.2",
55 | "@typescript-eslint/eslint-plugin": "^6.3.0",
56 | "@typescript-eslint/parser": "^6.3.0",
57 | "autoprefixer": "10.4.14",
58 | "encoding": "^0.1.13",
59 | "eslint": "8.46.0",
60 | "eslint-config-next": "13.4.13",
61 | "postcss": "8.4.27",
62 | "prettier": "^3.0.1",
63 | "prettier-plugin-tailwindcss": "^0.4.1",
64 | "tailwindcss": "3.3.3",
65 | "typescript": "5.1.6"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/web/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | plugins: [require.resolve("prettier-plugin-tailwindcss")],
4 | };
5 |
6 | module.exports = config;
7 |
--------------------------------------------------------------------------------
/web/public/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethndotsh/openbin/3d61b6e5ec26e82da539338d17271ed1ed589d7c/web/public/assets/favicon.png
--------------------------------------------------------------------------------
/web/public/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/public/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethndotsh/openbin/3d61b6e5ec26e82da539338d17271ed1ed589d7c/web/public/assets/image.png
--------------------------------------------------------------------------------
/web/public/assets/pfp-placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethndotsh/openbin/3d61b6e5ec26e82da539338d17271ed1ed589d7c/web/public/assets/pfp-placeholder.png
--------------------------------------------------------------------------------
/web/public/assets/profile-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethndotsh/openbin/3d61b6e5ec26e82da539338d17271ed1ed589d7c/web/public/assets/profile-bg.png
--------------------------------------------------------------------------------
/web/src/app/auth/callback/[redirect]/route.ts:
--------------------------------------------------------------------------------
1 | import { BASE_URL } from "@/utils/config";
2 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
3 | import { cookies } from "next/headers";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export async function GET(
7 | req: NextRequest,
8 | { params }: { params: { redirect: string } },
9 | ) {
10 | const supabase = createRouteHandlerClient({ cookies });
11 | const { searchParams } = new URL(req.url);
12 | const code = searchParams.get("code");
13 | const redirect = params.redirect;
14 |
15 | if (code) {
16 | await supabase.auth.exchangeCodeForSession(code);
17 | }
18 |
19 | if (redirect) {
20 | return NextResponse.redirect(
21 | new URL(decodeURIComponent(redirect), BASE_URL),
22 | );
23 | }
24 |
25 | return NextResponse.redirect(new URL(`/`, BASE_URL));
26 | }
27 |
--------------------------------------------------------------------------------
/web/src/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { BASE_URL } from "@/utils/config";
2 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
3 | import { cookies } from "next/headers";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export async function GET(req: NextRequest) {
7 | const supabase = createRouteHandlerClient({ cookies });
8 | const { searchParams } = new URL(req.url);
9 | const code = searchParams.get("code");
10 |
11 | if (code) {
12 | await supabase.auth.exchangeCodeForSession(code);
13 | }
14 |
15 | return NextResponse.redirect(new URL(`/`, BASE_URL));
16 | }
17 |
--------------------------------------------------------------------------------
/web/src/app/auth/delete/confirm/route.ts:
--------------------------------------------------------------------------------
1 | import { BASE_URL } from "@/utils/config";
2 | import {
3 | createRouteHandlerClient,
4 | createServerActionClient,
5 | createServerComponentClient,
6 | } from "@supabase/auth-helpers-nextjs";
7 | import { createClient } from "@supabase/supabase-js";
8 | import { cookies } from "next/headers";
9 | import { type NextRequest, NextResponse } from "next/server";
10 |
11 | export async function POST(req: NextRequest) {
12 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
13 | throw new Error("Missing env.NEXT_PUBLIC_SUPABASE_URL");
14 | }
15 |
16 | if (!process.env.SUPABASE_SERVICE_KEY) {
17 | throw new Error("Missing env.SUPABASE_SERVICE_KEY");
18 | }
19 |
20 | const supabase = createServerActionClient({ cookies: () => cookies() });
21 |
22 | const supabaseServer = createClient(
23 | process.env.NEXT_PUBLIC_SUPABASE_URL,
24 | process.env.SUPABASE_SERVICE_KEY,
25 | { auth: { persistSession: false } },
26 | );
27 |
28 | // Check if we have a session
29 | const {
30 | data: { session },
31 | error: sessionError,
32 | } = await supabase.auth.getSession();
33 |
34 | if (sessionError) {
35 | console.error("Error:", sessionError);
36 | return;
37 | }
38 |
39 | if (session) {
40 | await supabase.auth.signOut(); // to be safe
41 | await supabaseServer.auth.admin.signOut(session.access_token);
42 |
43 | const { data, error } = await supabaseServer.auth.admin.deleteUser(
44 | session.user.id,
45 | );
46 |
47 | if (error) {
48 | console.error("Error:", error);
49 | return;
50 | }
51 |
52 | return NextResponse.redirect(new URL("/", BASE_URL), {
53 | status: 302,
54 | });
55 | } else {
56 | return NextResponse.redirect(new URL("/", BASE_URL), {
57 | status: 302,
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/web/src/app/auth/delete/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Navbar } from "@/components/navbar";
2 | import {
3 | createServerComponentClient,
4 | getProfile,
5 | getSession,
6 | } from "@/utils/supabase";
7 | import { redirect } from "next/navigation";
8 | import { ReactNode } from "react";
9 |
10 | export default async function AuthDeleteLayout({
11 | children,
12 | }: {
13 | children: ReactNode;
14 | }) {
15 | const session = await getSession();
16 |
17 | if (!session) {
18 | return redirect("/");
19 | }
20 |
21 | const profile = await getProfile(session.user.id);
22 | return (
23 | <>
24 |
14 | Are you sure you want to delete your account? 15 |
16 |17 | This action is irreversible. All of your pastes will be deleted. 18 | Remixed pastes made by other users will be unaffected. 19 |
20 |65 | Openbin brings the ease of use of Pastebin and Gists to your 66 | terminal, allowing you to draft, publish and share text files in 67 | seconds. 68 |
69 |
107 |
113 |
126 | 127 | {pasteData.language} 128 | {" "} 129 | 130 | {pasteData.draft && "(draft)"} 131 | 132 |
133 |134 | {pasteData.created_at && ( 135 | 138 | )} 139 |
140 |{pasteData.description}
141 |17 | Last updated: August 10, 2023 18 |
19 |137 | {profile.id === session?.user.id && "Your "}Pastes 138 |
139 |{paste.language}
171 | 172 |175 | {(paste.description && !!paste.description.length) ?? 176 | "No description"} 177 |
178 | 179 | ))} 180 |No Pastes
185 |7 | Made with 💚 by{" "} 8 | 9 | Unnamed Engineering 10 | 11 |
12 |132 | kiwi@copple —{" "} 133 | {os === "macos" 134 | ? "zsh" 135 | : os === "windows" 136 | ? "powershell" 137 | : os === "linux" 138 | ? "terminal" 139 | : "..."}{" "} 140 |
141 |
145 | {displayText}
146 | {commandLine && (
147 |
151 | █
152 |
153 | )}
154 |
155 | {!rendering && (
156 | 161 | {body} 162 |
163 | ) 164 | }) 165 | FormMessage.displayName = "FormMessage" 166 | 167 | export { 168 | useFormField, 169 | Form, 170 | FormItem, 171 | FormLabel, 172 | FormControl, 173 | FormDescription, 174 | FormMessage, 175 | FormField, 176 | } 177 | -------------------------------------------------------------------------------- /web/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/utils/cn" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes