├── .gitignore ├── Makefile ├── README.md ├── backend ├── .Dockerignore ├── Dockerfile ├── Dockerfile-ci ├── api │ ├── api.go │ └── routes.go ├── cache │ └── cache.go ├── db │ ├── db.go │ ├── mongo.go │ └── schemas.go ├── go.mod ├── go.sum ├── main.go └── security │ ├── encrypt.go │ └── hash.go └── frontend ├── .babelrc ├── .gitignore ├── package.json ├── public └── favicon.png ├── src ├── components │ ├── Common │ │ ├── Button.js │ │ ├── Input.js │ │ └── mixins.js │ ├── Err.js │ ├── Footer.js │ ├── Inputs │ │ ├── Code.js │ │ ├── Dropdown.js │ │ ├── Password.js │ │ ├── Text.js │ │ └── index.js │ ├── NextHead.js │ ├── Options.js │ ├── PasteInfo.js │ ├── Watermark.js │ ├── decorators │ │ ├── CharLimit.js │ │ └── Labelled.js │ ├── modals │ │ ├── PasswordModal.js │ │ ├── PasteModal.js │ │ └── shared.js │ └── renderers │ │ ├── Code.js │ │ ├── InlineCode.js │ │ ├── Latex.js │ │ ├── Markdown.js │ │ └── RenderDispatch.js ├── http │ ├── resolvePaste.js │ └── shared.js ├── pages │ ├── [hash].js │ ├── _app.js │ ├── _document.js │ ├── index.js │ └── raw │ │ └── [hash].js └── theme │ ├── GlobalStyle.js │ └── ThemeProvider.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | backend/.env 2 | .idea 3 | .DS_Store 4 | 5 | frontend/node_modules 6 | frontend/build 7 | frontend/.pnp 8 | frontend/.next 9 | .pnp.js 10 | 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | help: ## Show all Makefile targets 4 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | run: ## Start backend 7 | cd backend && go run . 8 | lint: ## Lint backend 9 | cd backend && golangci-lint run 10 | docker-build: ## Docker build backend 11 | docker build -t ctrl-v:latest ./backend 12 | docker-run: docker-build ## Start dockerized backend 13 | docker run -p 8080:8080 ctrl-v:latest 14 | 15 | fe-run: ## Start Frontend 16 | cd frontend && yarn start 17 | fe-build: ## Productionize Frontend 18 | cd frontend && yarn build 19 | dev: ## Start backend and frontend 20 | make -j 2 run fe-run -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctrl-v 2 | ### A modern, open-source pastebin with latex and markdown rendering support 3 | Frontend is in React + Next.js and backend is in Go. Deployed via Vercel and Google Cloud Run. 4 | 5 | ![Go Paste Example](https://user-images.githubusercontent.com/23178940/83225601-06f0bb80-a135-11ea-9af2-9f2946459fe7.png) 6 | ![Markdown Rendering](https://user-images.githubusercontent.com/23178940/83225605-0821e880-a135-11ea-9efd-e7242ebde265.png) 7 | ![Showing off another theme!](https://user-images.githubusercontent.com/23178940/83225610-0a844280-a135-11ea-8c7c-4a0ecb13f379.png) 8 | ![Latex Rendering](https://user-images.githubusercontent.com/23178940/83225613-0c4e0600-a135-11ea-9f27-e5653cf9f343.png) 9 | 10 | ## Public API 11 | The ctrl-v API is provided for free for other developers to easily develop on top of it. It can be reached at `https://api.ctrl-v.app/`. 12 | 13 | ### `GET /health` 14 | ```bash 15 | # get the health of the API 16 | curl https://api.ctrl-v.app/health 17 | 18 | # 200 OK 19 | # > "status ok" 20 | ``` 21 | 22 | ### `POST /api` 23 | ```bash 24 | # create a new paste 25 | curl -L -X POST 'https://api.ctrl-v.app/api' \ 26 | -F 'expiry=2021-03-09T01:02:43.082Z' \ 27 | -F 'content=print(\"test content\")' \ 28 | -F 'title=test paste' \ 29 | -F 'language=python' 30 | 31 | # or with a password 32 | curl -L -X POST 'https://api.ctrl-v.app/api' \ 33 | -F 'expiry=2021-03-09T01:02:43.082Z' \ 34 | -F 'content=print(\"test content\")' \ 35 | -F 'title=test paste' \ 36 | -F 'language=python' \ 37 | -F 'password=hunter2' 38 | 39 | # 200 OK 40 | # > { "hash": "6Z7NVVv" } 41 | 42 | # 400 BAD_REQUEST 43 | # happens when title/body is too long, password couldnt 44 | # be hashed, or expiry is not in RFC3339 format 45 | ``` 46 | ### `GET /api/{hash}` 47 | ```bash 48 | # get unprotected hash 49 | curl https://api.ctrl-v.app/api/1t9UybX 50 | 51 | # 200 OK 52 | # > { 53 | # > "content": "print(\"test content\")", 54 | # > "expiry": "2021-03-09T01:02:43.082Z", 55 | # > "language": "python", 56 | # > "timestamp": "2021-03-02T01:06:16.209501971Z", 57 | # > "title": "test paste" 58 | # > } 59 | 60 | # 401 BAD_REQUEST 61 | # happens when paste is password protected. when this happens, try the authenticated alternative using POST 62 | # 404 NOT_FOUND 63 | # no paste with that ID found 64 | ``` 65 | 66 | ### `POST /api/{hash}` 67 | ```bash 68 | # get unprotected hash 69 | curl -L -X POST 'https://api.ctrl-v.app/api/1t9UybX' \ 70 | -F 'password=hunter2' 71 | 72 | # 200 OK 73 | # > { 74 | # > "content": "print(\"test content\")", 75 | # > "expiry": "2021-03-09T01:02:43.082Z", 76 | # > "language": "python", 77 | # > "timestamp": "2021-03-02T01:06:16.209501971Z", 78 | # > "title": "test paste" 79 | # > } 80 | 81 | # 401 BAD_REQUEST 82 | # wrong password 83 | # 404 NOT_FOUND 84 | # no paste with that ID found 85 | ``` 86 | 87 | ## Developing 88 | when doing local backend development, make sure you change the backend address to be localhost. You can find this on Line 4 of `frontend/src/http/shared.js` 89 | 90 | ### Common 91 | `make dev` — starts React development server on `:3000` and backend on `:8080` 92 | 93 | ### Frontend 94 | `make fe-run` — starts React development server on `:3000` 95 | 96 | `make fe-build` — builds development release of frontend in `frontend/build` 97 | 98 | ### Backend 99 | `make run` — starts backend on `:8080` 100 | 101 | `make lint` — lints all Go files 102 | 103 | `make docker-build` — builds Docker image of current backend 104 | 105 | `make docker-run` — runs built Docker image on `:8080` -------------------------------------------------------------------------------- /backend/.Dockerignore: -------------------------------------------------------------------------------- 1 | frontend -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Build stage 2 | FROM golang:alpine AS builder 3 | ENV GO111MODULE=on 4 | 5 | # Copy files to image 6 | COPY . /app/src 7 | WORKDIR /app/src 8 | 9 | RUN apk add git ca-certificates 10 | 11 | # Build image 12 | RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app 13 | 14 | ## Image creation stage 15 | FROM scratch 16 | 17 | # Copy app 18 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 19 | COPY --from=builder /go/bin/app ./ 20 | COPY .env ./ 21 | 22 | # Expose ports, change port to whatever you need to expose 23 | EXPOSE 8080 24 | 25 | # Run app 26 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /backend/Dockerfile-ci: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | ENV GO111MODULE=on 3 | COPY . /app/src 4 | WORKDIR /app/src 5 | RUN apk add git ca-certificates 6 | RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app 7 | 8 | FROM scratch 9 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 10 | COPY --from=builder /go/bin/app ./ 11 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /backend/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | "syscall" 10 | "time" 11 | 12 | mux "github.com/gorilla/mux" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/jackyzha0/ctrl-v/db" 15 | ) 16 | 17 | func cleanup() { 18 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 19 | defer cancel() 20 | if err := db.Client.Disconnect(ctx); err != nil { 21 | panic(err) 22 | } 23 | log.Print("Shutting down server...") 24 | } 25 | 26 | // Define router and start server 27 | func Serve(port int) { 28 | // Sigint trapper 29 | c := make(chan os.Signal) 30 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 31 | go func() { 32 | <-c 33 | cleanup() 34 | os.Exit(0) 35 | }() 36 | 37 | // Define Mux Router 38 | r := mux.NewRouter() 39 | r.HandleFunc("/health", healthCheckFunc) 40 | r.HandleFunc("/api", insertFunc).Methods("POST", "OPTIONS") 41 | r.HandleFunc("/api/{hash}", getPasteFunc).Methods("GET", "OPTIONS") 42 | r.HandleFunc("/api/{hash}", getPasteWithPasswordFunc).Methods("POST", "OPTIONS") 43 | 44 | http.Handle("/", r) 45 | 46 | // Start HTTP server 47 | server := newServer(":"+strconv.Itoa(port), r) 48 | log.Printf("Starting server on %d", port) 49 | 50 | defer cleanup() 51 | err := server.ListenAndServe() 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | 57 | // Function to create new HTTP server 58 | func newServer(addr string, router http.Handler) *http.Server { 59 | return &http.Server{ 60 | Addr: addr, 61 | Handler: router, 62 | ReadTimeout: time.Second * 30, 63 | WriteTimeout: time.Second * 30, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/jackyzha0/ctrl-v/security" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/jackyzha0/ctrl-v/cache" 13 | "github.com/jackyzha0/ctrl-v/db" 14 | ) 15 | 16 | func healthCheckFunc(w http.ResponseWriter, r *http.Request) { 17 | fmt.Fprint(w, "status ok") 18 | } 19 | 20 | func insertFunc(w http.ResponseWriter, r *http.Request) { 21 | 22 | // Allow CORS 23 | w.Header().Set("Access-Control-Allow-Origin", "*") 24 | 25 | // get content 26 | _ = r.ParseMultipartForm(0) 27 | expiry := r.FormValue("expiry") 28 | content := r.FormValue("content") 29 | title := r.FormValue("title") 30 | password := r.FormValue("password") 31 | lang := r.FormValue("language") 32 | 33 | // insert content 34 | hash, err := db.New(content, expiry, title, password, lang) 35 | if err != nil { 36 | w.WriteHeader(http.StatusBadRequest) 37 | fmt.Fprintf(w, "%s", err.Error()) 38 | return 39 | } 40 | 41 | // if successful return paste hash 42 | w.Header().Set("Content-Type", "application/json") 43 | pasteMap := map[string]interface{}{ 44 | "hash": hash, 45 | } 46 | 47 | jsonData, _ := json.Marshal(pasteMap) 48 | fmt.Fprintf(w, "%+v", string(jsonData)) 49 | } 50 | 51 | func getPasteFunc(w http.ResponseWriter, r *http.Request) { 52 | // no password given for get 53 | handleGetPaste(w, r, "") 54 | } 55 | 56 | func getPasteWithPasswordFunc(w http.ResponseWriter, r *http.Request) { 57 | // get password from form 58 | _ = r.ParseMultipartForm(0) 59 | parsedPassword := r.FormValue("password") 60 | 61 | handleGetPaste(w, r, parsedPassword) 62 | 63 | } 64 | 65 | func handleGetPaste(w http.ResponseWriter, r *http.Request, parsedPassword string) { 66 | // Allow CORS 67 | w.Header().Set("Access-Control-Allow-Origin", "*") 68 | 69 | hash := mux.Vars(r)["hash"] 70 | paste, err := cache.C.Get(hash, parsedPassword) 71 | 72 | // if hash was not found 73 | if err == cache.PasteNotFound { 74 | w.WriteHeader(http.StatusNotFound) 75 | fmt.Fprintf(w, "%s", err) 76 | return 77 | } 78 | 79 | // if paste is password protected 80 | if err == cache.UserUnauthorized { 81 | w.WriteHeader(http.StatusUnauthorized) 82 | fmt.Fprintf(w, "%s", err) 83 | return 84 | } 85 | 86 | // if internal error with encryption 87 | if err == security.EncryptionError { 88 | w.WriteHeader(http.StatusInternalServerError) 89 | fmt.Fprintf(w, "%s", err) 90 | return 91 | } 92 | 93 | // otherwise, return paste content, title, and current time 94 | w.Header().Set("Content-Type", "application/json") 95 | pasteMap := map[string]interface{}{ 96 | "timestamp": time.Now(), 97 | "title": paste.Title, 98 | "content": paste.Content, 99 | "expiry": paste.Expiry, 100 | "language": paste.Language, 101 | } 102 | 103 | jsonData, _ := json.Marshal(pasteMap) 104 | fmt.Fprintf(w, "%+v", string(jsonData)) 105 | } 106 | -------------------------------------------------------------------------------- /backend/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/jackyzha0/ctrl-v/security" 9 | 10 | "github.com/jackyzha0/ctrl-v/db" 11 | ) 12 | 13 | type Cache struct { 14 | m map[string]db.Paste 15 | lock sync.RWMutex 16 | } 17 | 18 | var C *Cache 19 | 20 | var PasteNotFound = errors.New("could not find a paste with that hash") 21 | var UserUnauthorized = errors.New("paste is password protected") 22 | 23 | func init() { 24 | C = &Cache{ 25 | m: map[string]db.Paste{}, 26 | } 27 | } 28 | 29 | func (c *Cache) Get(hash, userPassword string) (db.Paste, error) { 30 | c.lock.RLock() 31 | 32 | // check if hash in cache 33 | p, ok := c.m[hash] 34 | c.lock.RUnlock() 35 | 36 | // if it doesnt, lookup from db 37 | if !ok { 38 | var err error 39 | 40 | p, err = db.Lookup(hash) 41 | if err != nil { 42 | return db.Paste{}, PasteNotFound 43 | } 44 | 45 | c.add(p) 46 | } 47 | 48 | // check if expired 49 | if time.Now().After(p.Expiry) { 50 | c.evict(p) 51 | return db.Paste{}, PasteNotFound 52 | } 53 | 54 | // if there is a password, check the provided one against it 55 | if p.Password != "" { 56 | // if passwords do not match, the user is unauthorized 57 | if !security.PasswordsEqual(p.Password, userPassword) { 58 | return db.Paste{}, UserUnauthorized 59 | } 60 | 61 | // if password matches, decrypt content 62 | key, _, err := security.DeriveKey(userPassword, p.Salt) 63 | if err != nil { 64 | return db.Paste{}, security.EncryptionError 65 | } 66 | 67 | decryptedContent, err := security.Decrypt(key, p.Content) 68 | if err != nil { 69 | return db.Paste{}, security.EncryptionError 70 | } 71 | 72 | p.Content = decryptedContent 73 | } 74 | 75 | return p, nil 76 | } 77 | 78 | func (c *Cache) add(p db.Paste) { 79 | c.lock.Lock() 80 | defer c.lock.Unlock() 81 | 82 | c.m[p.Hash] = p 83 | } 84 | 85 | func (c *Cache) evict(p db.Paste) { 86 | c.lock.Lock() 87 | defer c.lock.Unlock() 88 | 89 | delete(c.m, p.Hash) 90 | } 91 | -------------------------------------------------------------------------------- /backend/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/jackyzha0/ctrl-v/security" 9 | "github.com/joho/godotenv" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func init() { 14 | // load .env file 15 | err := godotenv.Load() 16 | if err != nil { 17 | log.Warnf("Error loading .env file: %s", err.Error()) 18 | log.Warn("Falling back on env vars...") 19 | } 20 | 21 | mUser := os.Getenv("MONGO_USER") 22 | mPass := os.Getenv("MONGO_PASS") 23 | mIP := os.Getenv("MONGO_SHARD_URL") 24 | 25 | initSessions(mUser, mPass, mIP) 26 | } 27 | 28 | const TitleLimit = 100 29 | const ContentLimit = 100000 30 | 31 | // creates a new paste with title, content and hash, returns the hash of the created paste 32 | func New(content, expiry, title, password, lang string) (string, error) { 33 | // generate hash from ip 34 | hash := security.GenerateURI(content) 35 | 36 | // check for size of title and content 37 | errs := checkLengths(title, content) 38 | if errs != nil { 39 | return "", errs 40 | } 41 | 42 | // create new struct 43 | new := Paste{ 44 | Hash: hash, 45 | Content: content, 46 | Title: title, 47 | Language: lang, 48 | } 49 | 50 | // if there is a password, encrypt content and hash the password 51 | if password != "" { 52 | // use pass to encrypt content 53 | key, salt, err := security.DeriveKey(password, nil) 54 | if err != nil { 55 | return "", fmt.Errorf("could not generate key: %s", err.Error()) 56 | } 57 | new.Salt = salt 58 | 59 | encryptedContent, err := security.Encrypt(key, new.Content) 60 | if err != nil { 61 | return "", fmt.Errorf("could not encrypt content: %s", err.Error()) 62 | } 63 | 64 | new.Content = encryptedContent 65 | 66 | // hash given password 67 | hashedPass, err := security.HashPassword(password) 68 | if err != nil { 69 | return "", fmt.Errorf("could not hash password: %s", err.Error()) 70 | } 71 | new.Password = hashedPass 72 | } 73 | 74 | // check if expiry 75 | if expiry != "" { 76 | t, err := time.Parse(time.RFC3339, expiry) 77 | 78 | // if time format not current 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | // time is in the past 84 | if time.Now().After(t) { 85 | return "", fmt.Errorf("time %s is in the past", t.String()) 86 | } 87 | 88 | new.Expiry = t 89 | 90 | } else { 91 | // 5 year expiry 92 | new.Expiry = time.Now().Add(time.Hour * 43800) 93 | } 94 | 95 | // insert struct 96 | log.Infof("create new paste with hash %s", hash) 97 | insertErr := insert(new) 98 | return hash, insertErr 99 | } 100 | 101 | func checkLengths(title string, content string) error { 102 | if len(title) > TitleLimit { 103 | return fmt.Errorf("title is longer than character limit of %d\n", TitleLimit) 104 | } 105 | if len(content) > ContentLimit { 106 | return fmt.Errorf("content is longer than character limit of %d\n", ContentLimit) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // lookup 113 | func Lookup(hash string) (Paste, error) { 114 | return fetch(hash) 115 | } 116 | -------------------------------------------------------------------------------- /backend/db/mongo.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "go.mongodb.org/mongo-driver/mongo/readpref" 14 | ) 15 | 16 | var Client *mongo.Client 17 | var Session *mongo.Session 18 | var pastes *mongo.Collection 19 | 20 | func initSessions(user, pass, ip string) { 21 | log.Infof("attempting connection to %s", ip) 22 | 23 | // build uri string 24 | URIfmt := "mongodb://%s:%s@%s:27017" 25 | mongoURI := fmt.Sprintf(URIfmt, user, pass, ip) 26 | 27 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 28 | defer cancel() 29 | c, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI).SetTLSConfig(&tls.Config{})) 30 | Client = c 31 | if err != nil { 32 | log.Fatalf("error establishing connection to mongo: %s", err.Error()) 33 | } 34 | err = Client.Ping(ctx, readpref.Primary()) 35 | if err != nil { 36 | log.Fatalf("error pinging mongo: %s", err.Error()) 37 | } 38 | 39 | // ensure expiry check 40 | expiryIndex := options.Index().SetExpireAfterSeconds(0) 41 | sessionTTL := mongo.IndexModel{ 42 | Keys: []string{"expiry"}, 43 | Options: expiryIndex, 44 | } 45 | 46 | // ensure hashes are unique 47 | uniqueIndex := options.Index().SetUnique(true) 48 | uniqueHashes := mongo.IndexModel{ 49 | Keys: []string{"hash"}, 50 | Options: uniqueIndex, 51 | } 52 | 53 | // Define connection to Databases 54 | pastes = Client.Database("main").Collection("pastes") 55 | _, _ = pastes.Indexes().CreateOne(ctx, sessionTTL) 56 | _, _ = pastes.Indexes().CreateOne(ctx, uniqueHashes) 57 | } 58 | 59 | func insert(new Paste) error { 60 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 61 | defer cancel() 62 | _, err := pastes.InsertOne(ctx, new) 63 | return err 64 | } 65 | 66 | func fetch(hash string) (Paste, error) { 67 | p := Paste{} 68 | q := bson.M{"hash": hash} 69 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 70 | defer cancel() 71 | err := pastes.FindOne(ctx, q).Decode(&p) 72 | return p, err 73 | } 74 | -------------------------------------------------------------------------------- /backend/db/schemas.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // Paste represents a single paste 10 | type Paste struct { 11 | ID primitive.ObjectID `bson:"_id,omitempty"` 12 | Hash string 13 | Title string 14 | Content string 15 | Language string 16 | Password string 17 | Expiry time.Time `bson:"expiry"` 18 | Salt []byte 19 | } 20 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jackyzha0/ctrl-v 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/mux v1.7.4 7 | github.com/joho/godotenv v1.3.0 8 | github.com/kr/pretty v0.2.0 // indirect 9 | github.com/sirupsen/logrus v1.6.0 10 | go.mongodb.org/mongo-driver v1.12.0 11 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d 12 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 13 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 3 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 4 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 8 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 9 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 10 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 11 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 12 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 13 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= 17 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 20 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 23 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 24 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 25 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 26 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 27 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 28 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 29 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 30 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 31 | go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= 32 | go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 35 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= 36 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 37 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 38 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 40 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 41 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 42 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 44 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 52 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 54 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 55 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 56 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 57 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 58 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 59 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 60 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 61 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 63 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 64 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 65 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 66 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 67 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jackyzha0/ctrl-v/api" 5 | _ "github.com/jackyzha0/ctrl-v/cache" // setup cache 6 | _ "github.com/jackyzha0/ctrl-v/db" // setup db 7 | ) 8 | 9 | func main() { 10 | api.Serve(8080) 11 | } 12 | -------------------------------------------------------------------------------- /backend/security/encrypt.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "errors" 8 | "golang.org/x/crypto/scrypt" 9 | ) 10 | 11 | var EncryptionError = errors.New("could not encrypt the given content") 12 | 13 | func Encrypt(key, data string) (string, error) { 14 | // initialize aes block cipher with given key 15 | blockCipher, err := aes.NewCipher([]byte(key)) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | // wrap block cipher with Galois Counter Mode and standard nonce length 21 | gcm, err := cipher.NewGCM(blockCipher) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | // generate nonce (number once used) unique to the given key 27 | nonce := make([]byte, gcm.NonceSize()) 28 | if _, err = rand.Read(nonce); err != nil { 29 | return "", err 30 | } 31 | 32 | // seal nonce with data to use during decryption 33 | cipherText := gcm.Seal(nonce, nonce, []byte(data), nil) 34 | 35 | return string(cipherText), nil 36 | } 37 | 38 | func Decrypt(key, data string) (string, error) { 39 | // similar to encrypt, create cipher and wrap with GCM 40 | blockCipher, err := aes.NewCipher([]byte(key)) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | gcm, err := cipher.NewGCM(blockCipher) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | // extract the nonce from the data 51 | nonce, cipherText := data[:gcm.NonceSize()], data[gcm.NonceSize():] 52 | 53 | // use nonce to decrypt the data 54 | plaintext, err := gcm.Open(nil, []byte(nonce), []byte(cipherText), nil) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | return string(plaintext), nil 60 | } 61 | 62 | const keyBytes = 16 63 | const iterations = 16384 64 | const relativeMemoryCost = 8 65 | const relativeCPUCost = 1 66 | 67 | func DeriveKey(password string, salt []byte) (string, []byte, error) { 68 | if salt == nil { 69 | salt = make([]byte, keyBytes) 70 | if _, err := rand.Read(salt); err != nil { 71 | return "", nil, err 72 | } 73 | } 74 | 75 | key, err := scrypt.Key([]byte(password), salt, iterations, relativeMemoryCost, relativeCPUCost, keyBytes) 76 | if err != nil { 77 | return "", nil, err 78 | } 79 | 80 | return string(key), salt, nil 81 | } 82 | -------------------------------------------------------------------------------- /backend/security/hash.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "golang.org/x/crypto/bcrypt" 7 | "math/big" 8 | "time" 9 | ) 10 | 11 | const UrlLength = 7 12 | 13 | // GenerateURI creates a unique identifier for a paste based on ip and timestamp 14 | func GenerateURI(content string) string { 15 | timeStamp := time.Now().String() 16 | return hashString(content + timeStamp)[:UrlLength] 17 | } 18 | 19 | // hashes using MD5 and then converts to base 62 20 | func hashString(text string) string { 21 | hash := md5.Sum([]byte(text)) 22 | hexStr := hex.EncodeToString(hash[:]) 23 | 24 | bi := big.NewInt(0) 25 | bi.SetString(hexStr, 16) 26 | return bi.Text(62) 27 | } 28 | 29 | func HashPassword(password string) (string, error) { 30 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 31 | return string(hashedPassword), err 32 | } 33 | 34 | func PasswordsEqual(dbPassword, parsedPassword string) bool { 35 | dbPassBytes := []byte(dbPassword) 36 | parsedPassBytes := []byte(parsedPassword) 37 | compErr := bcrypt.CompareHashAndPassword(dbPassBytes, parsedPassBytes) 38 | 39 | // if comparison error, the given password is not valid 40 | return compErr == nil 41 | } -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "styled-components", 8 | { 9 | "ssr": true, 10 | "displayName": true, 11 | "preprocess": false 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.1", 7 | "next": "^10.1.3", 8 | "react": "^16.13.1", 9 | "react-dom": "^16.13.1", 10 | "react-dropdown": "^1.7.0", 11 | "react-katex": "^2.0.2", 12 | "react-markdown": "^4.3.1", 13 | "react-modal": "^3.11.2", 14 | "react-simple-code-editor": "^0.11.0", 15 | "react-syntax-highlighter": "^12.2.1", 16 | "react-syntax-highlighter-virtualized-renderer": "^1.1.0", 17 | "styled-components": "^5.1.0", 18 | "use-clipboard-copy": "^0.1.2" 19 | }, 20 | "scripts": { 21 | "dev": "next dev", 22 | "start": "next start", 23 | "build": "next build" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "babel-plugin-styled-components": "^1.12.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackyzha0/ctrl-v/9853ff01573b9495da848e09a62cd11add19e4bf/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/src/components/Common/Button.js: -------------------------------------------------------------------------------- 1 | import styled, {css} from 'styled-components' 2 | import {Border, ButtonLike, DropShadow, Rounded} from "./mixins"; 3 | 4 | const Base = css` 5 | ${DropShadow} 6 | ${Rounded} 7 | ${ButtonLike} 8 | margin-right: 2em; 9 | height: calc(16px + 1.4em); 10 | cursor: pointer; 11 | ` 12 | 13 | const Primary = css` 14 | ${Base}; 15 | border: none; 16 | color: ${p => p.theme.colors.background}; 17 | background-color: ${p => p.theme.colors.text}; 18 | ` 19 | const Secondary = css` 20 | ${Base}; 21 | ${Border}; 22 | color: ${p => p.theme.colors.text}; 23 | background-color: ${p => p.theme.colors.background}; 24 | ` 25 | 26 | export const Button = styled.button` 27 | ${p => p.secondary ? css`${Secondary}` : css`${Primary}` } 28 | ` 29 | 30 | export const SubmitButton = styled.input` 31 | ${Primary} 32 | ` -------------------------------------------------------------------------------- /frontend/src/components/Common/Input.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import {Border, DropShadow, InputLike, Rounded} from "./mixins"; 3 | 4 | export const Input = styled.input` 5 | ${Border} 6 | ${Rounded} 7 | ${DropShadow} 8 | ${InputLike} 9 | ` -------------------------------------------------------------------------------- /frontend/src/components/Common/mixins.js: -------------------------------------------------------------------------------- 1 | import {css} from 'styled-components'; 2 | 3 | export const DropShadow = css` 4 | box-shadow: 0 14px 28px rgba(27, 33, 48,0.06), 0 10px 10px rgba(27, 33, 48,0.02); 5 | ` 6 | 7 | export const Hover = css` 8 | opacity: 0.5; 9 | transition: all 0.5s cubic-bezier(.25,.8,.25,1); 10 | 11 | & ~ pre { 12 | transition: all 0.5s cubic-bezier(.25,.8,.25,1); 13 | opacity: 0.5; 14 | } 15 | 16 | &:focus, &:hover, &:focus span, &:focus ~ pre { 17 | opacity: 1; 18 | } 19 | ` 20 | 21 | export const Rounded = css` 22 | border-radius: 3px; 23 | ` 24 | 25 | export const Border = css` 26 | border: 1px solid ${p => p.theme.colors.border}; 27 | ` 28 | 29 | export const InputLike = css` 30 | ${Hover} 31 | font-family: 'JetBrains Mono', monospace; 32 | width: 100%; 33 | font-size: 0.8em; 34 | padding: 0.6em; 35 | outline: none; 36 | margin: 1.7em 0; 37 | ` 38 | 39 | export const CodeLike = css` 40 | font-family: JetBrains Mono !important; 41 | font-size: 13px !important; 42 | line-height: 1.6em !important; 43 | ` 44 | 45 | export const ButtonLike = css` 46 | font-family: 'JetBrains Mono', serif; 47 | font-weight: 700; 48 | padding: 0.6em 1.5em; 49 | margin: 2em 0; 50 | outline: 0; 51 | ` 52 | -------------------------------------------------------------------------------- /frontend/src/components/Err.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components' 3 | 4 | export const ErrMsg = styled.p` 5 | display: inline; 6 | font-weight: 700; 7 | color: #ff3333; 8 | opacity: 0; 9 | transition: opacity 0.3s cubic-bezier(.25,.8,.25,1); 10 | 11 | ${props => 12 | (props.active) && css` 13 | opacity: 1 14 | ` 15 | }; 16 | ` 17 | 18 | class Error extends React.Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | active: false, 25 | msg: ' ', 26 | }; 27 | 28 | this.showMessage = this.showMessage.bind(this); 29 | } 30 | 31 | showMessage(msg, duration = 3000) { 32 | this.setState({ 33 | active: true, 34 | msg: msg 35 | }) 36 | 37 | // fadeout after duration ms if duration != -1 38 | if (duration !== -1) { 39 | setTimeout(() => { 40 | this.setState({ active: false }) 41 | }, duration); 42 | } 43 | } 44 | 45 | render() { 46 | const msg = this.state.msg.toString().toLowerCase() 47 | return ( 48 | {msg} 49 | ); 50 | } 51 | } 52 | 53 | export default Error -------------------------------------------------------------------------------- /frontend/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const SpacedFooter = styled.div` 5 | & > p { 6 | font-size: 0.8em; 7 | } 8 | & a { 9 | color: ${(p) => p.theme.colors.text}; 10 | } 11 | `; 12 | 13 | const Link = (props) => { 14 | return ( 15 | 16 | {props.name} 17 | 18 | ); 19 | }; 20 | 21 | const Footer = () => { 22 | return ( 23 | 24 |

25 | (c) 2022 // ,{" "} 26 | 27 |

28 |
29 | ); 30 | }; 31 | 32 | export default Footer; 33 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/Code.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from 'styled-components' 3 | import CharLimit from "../decorators/CharLimit"; 4 | import Editor from 'react-simple-code-editor'; 5 | import {Highlighter} from "../renderers/Code"; 6 | import {CodeLike, Hover} from "../Common/mixins"; 7 | 8 | const Wrapper = styled.div` 9 | display: block; 10 | position: relative; 11 | width: calc(100%); 12 | ` 13 | 14 | const EditorWrapper = styled(Editor)` 15 | overflow: visible !important; 16 | position: relative; 17 | 18 | & > * { 19 | padding: 0 !important; 20 | width: 100%; 21 | } 22 | 23 | & pre, & code, & > textarea { 24 | ${CodeLike} 25 | min-height: 40vh; 26 | } 27 | 28 | & pre { 29 | z-index: -1 !important; 30 | } 31 | 32 | & > textarea { 33 | ${Hover} 34 | padding: 0.6em !important; 35 | outline: none !important; 36 | } 37 | ` 38 | 39 | export const Code = ({content, id, readOnly, setContentCallback, ...props}) => { 40 | return ( 41 | 42 | {code} } 50 | onValueChange={code => setContentCallback(code)} 51 | padding={15} 52 | /> 53 | 56 | 57 | ); 58 | } -------------------------------------------------------------------------------- /frontend/src/components/Inputs/Dropdown.js: -------------------------------------------------------------------------------- 1 | import Dropdown from "react-dropdown"; 2 | import React from "react"; 3 | import styled from 'styled-components'; 4 | import {LANGS, THEMES} from "../renderers/Code"; 5 | import {Labelled} from "../decorators/Labelled"; 6 | import {Border, DropShadow, InputLike, Rounded} from "../Common/mixins"; 7 | 8 | const StyledDropdown = styled(Dropdown)` 9 | ${Border} 10 | ${Rounded} 11 | ${DropShadow} 12 | ${InputLike} 13 | cursor: pointer; 14 | 15 | & .Dropdown-root { 16 | cursor: pointer; 17 | 18 | &:hover, &.is-open { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | & .Dropdown-placeholder { 24 | width: 5.5em; 25 | } 26 | 27 | & .Dropdown-menu { 28 | border-top: 1px solid ${p => p.theme.colors.text}; 29 | margin-top: 0.5em; 30 | bottom: auto; 31 | } 32 | 33 | & .Dropdown-option { 34 | margin-top: 0.5em; 35 | transition: all 0.5s cubic-bezier(.25,.8,.25,1); 36 | 37 | &:hover { 38 | font-weight: 700; 39 | opacity: 0.4; 40 | } 41 | } 42 | ` 43 | 44 | const GenericDropdown = (props) => { 45 | function _onSelect(option) { 46 | props.onChange({ 47 | target: { 48 | name: props.label, 49 | value: option.label 50 | } 51 | }); 52 | } 53 | 54 | return ( 55 | 59 | 65 | 66 | ); 67 | } 68 | 69 | export const Expiry = (props) => { 70 | const options = [ 71 | '5 years', 72 | '1 year', 73 | '1 month', 74 | '1 week', 75 | '1 day', 76 | '1 hour', 77 | '10 min', 78 | ]; 79 | 80 | return ( 81 | 87 | ); 88 | } 89 | 90 | export const Language = (props) => { 91 | const options = Object.entries(LANGS).map((key, _) => ({ 92 | 'value': key[1], 93 | 'label': key[0] 94 | })) 95 | 96 | return ( 97 | 103 | ); 104 | } 105 | 106 | export const Theme = (props) => { 107 | const options = Object.entries(THEMES).map((key) => ({ 108 | 'value': key[1], 109 | 'label': key[0] 110 | })) 111 | 112 | return ( 113 | 119 | ); 120 | } -------------------------------------------------------------------------------- /frontend/src/components/Inputs/Password.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Labelled } from "../decorators/Labelled"; 3 | import { Input } from "../Common/Input"; 4 | 5 | export const Password = (props) => ( 6 | 7 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /frontend/src/components/Inputs/Text.js: -------------------------------------------------------------------------------- 1 | import CharLimit from "../decorators/CharLimit"; 2 | import React from "react"; 3 | import {Labelled} from "../decorators/Labelled"; 4 | import {Input} from "../Common/Input"; 5 | 6 | export const Text = React.forwardRef(({label, id, readOnly, onChange, value, maxLength, autoFocus}, ref) => { 7 | return ( 8 | 9 | 20 | 23 | 24 | ); 25 | }) -------------------------------------------------------------------------------- /frontend/src/components/Inputs/index.js: -------------------------------------------------------------------------------- 1 | import {Code} from './Code'; 2 | import {Expiry, Language, Theme} from "./Dropdown"; 3 | import {Password} from "./Password"; 4 | import {Text} from "./Text"; 5 | 6 | export {Code, Expiry, Language, Theme, Password, Text}; -------------------------------------------------------------------------------- /frontend/src/components/NextHead.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | 3 | const NextHead = ({data}) => { 4 | const title = data.title || "untitled paste" 5 | const description = `${data.content.slice(0, 100)}... expires: ${data.expiry}` 6 | return ( 7 | ctrl-v | {title} 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default NextHead -------------------------------------------------------------------------------- /frontend/src/components/Options.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Password, Expiry, Language } from "./Inputs"; 4 | 5 | const Flex = styled.div` 6 | float: right; 7 | display: flex; 8 | flex-direction: row; 9 | transform: translateY(0.2em); 10 | 11 | @media (min-width: 650px) { 12 | & > *:not(:first-child) { 13 | margin-left: 2em; 14 | } 15 | } 16 | 17 | @media (max-width: 650px) { 18 | position: relative; 19 | float: none !important; 20 | flex-direction: column; 21 | } 22 | `; 23 | 24 | const OptionsContainer = ({ 25 | pass, 26 | lang, 27 | expiry, 28 | onPassChange, 29 | onLangChange, 30 | onExpiryChange, 31 | }) => { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default OptionsContainer; 42 | -------------------------------------------------------------------------------- /frontend/src/components/PasteInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components' 3 | import { Theme } from './Inputs' 4 | import {Button} from "./Common/Button"; 5 | import {useRouter} from "next/router"; 6 | import {ErrMsg} from "./Err"; 7 | 8 | const Bold = styled.span` 9 | font-weight: 700 10 | ` 11 | 12 | const StyledDiv = styled.div` 13 | display: inline-block; 14 | margin: 2em 0; 15 | ` 16 | 17 | const ShiftedButton = styled(Button)` 18 | margin-top: 1.6em !important; 19 | ` 20 | 21 | const SpacedText = styled.span` 22 | margin-right: 1em; 23 | ` 24 | 25 | const Flex = styled.div` 26 | float: right; 27 | display: flex; 28 | flex-direction: row; 29 | ` 30 | 31 | const PasteInfo = ({hash, lang, theme, expiry, toggleRenderCallback, isRenderMode, onChange, err}) => { 32 | const router = useRouter() 33 | const redirRaw = () => { 34 | const redirUrl = `/raw/${hash}` 35 | router.push(redirUrl); 36 | } 37 | 38 | const renderable = () => { 39 | const buttonTxt = isRenderMode ? 'text' : 'render' 40 | if (lang === 'latex' || lang === 'markdown') { 41 | return ( 42 | 46 | {buttonTxt} 47 | 48 | ); 49 | } 50 | } 51 | 52 | return ( 53 |
54 | 55 | 59 | view raw 60 | 61 | {renderable()} 62 | 66 | 67 | 68 | {err ? 69 | {err} : 70 | <> 71 | 72 | language: {lang} 73 | 74 | 75 | expires: {expiry} 76 | 77 | 78 | } 79 | 80 |
81 | ); 82 | } 83 | 84 | export default PasteInfo -------------------------------------------------------------------------------- /frontend/src/components/Watermark.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import React from "react"; 3 | import Footer from "./Footer"; 4 | 5 | const Logo = styled.div` 6 | position: fixed; 7 | bottom: 1em; 8 | left: 2em; 9 | opacity: 0.3; 10 | transition: opacity 0.5s cubic-bezier(0.25, 0.8, 0.25, 1); 11 | 12 | &:hover { 13 | opacity: 1; 14 | } 15 | 16 | & > h1 { 17 | font-size: 50px; 18 | margin: 0 0; 19 | 20 | & > a { 21 | text-decoration: none; 22 | position: relative; 23 | color: ${(p) => p.theme.colors.text}; 24 | } 25 | } 26 | 27 | @media (max-width: 960px) { 28 | position: relative; 29 | display: grid; 30 | left: -6em; 31 | font-size: 10px; 32 | } 33 | `; 34 | export const Watermark = () => ( 35 | 36 |

37 | ctrl-v 38 |

39 |