Compare commits
No commits in common. "f6192f61180ed2a2021df32f6c9a90f3394aa625" and "7f07e4012233dfd34f362128ccb38a5cf7749206" have entirely different histories.
f6192f6118
...
7f07e40122
17 changed files with 104 additions and 150 deletions
|
@ -1,4 +1,5 @@
|
||||||
when:
|
when:
|
||||||
|
- event: "push"
|
||||||
- event: "manual"
|
- event: "manual"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23.3 AS builder
|
FROM golang:1.23.1 AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN go build -o /bin/passed .
|
RUN go build -o /bin/passed .
|
||||||
|
|
24
README.md
24
README.md
|
@ -1,5 +1,4 @@
|
||||||
# PassED
|
# PassED
|
||||||
|
|
||||||
[](https://discord.gg/NuGxJKtDKS)
|
[](https://discord.gg/NuGxJKtDKS)
|
||||||
[](https://passed.1e99.eu/)
|
[](https://passed.1e99.eu/)
|
||||||
[](https://go.dev/)
|
[](https://go.dev/)
|
||||||
|
@ -17,30 +16,25 @@ You want to share it on paper, but everyone can read that too.
|
||||||
PassED solves this issue by allowing you to generate single-use URLs with your password.
|
PassED solves this issue by allowing you to generate single-use URLs with your password.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
When you generate a URL...
|
When you generate a URL...
|
||||||
|
|
||||||
1. The browser generates an AES key.
|
1. The browser generates an AES key.
|
||||||
2. The password you entered gets encrypted using this key.
|
2. The password you entered gets encrypted using this key.
|
||||||
3. The encrypted password is uploaded to the server, which responds with an ID to uniquely identify the password.
|
3. The encrypted password is uploaded to the server, which responds with an ID to uniquely identify the password.
|
||||||
4. A URL is generated that contains the ID and AES key.
|
4. A URL is generated that contains the ID and AES key.
|
||||||
|
|
||||||
When you view a password...
|
When you view a password...
|
||||||
|
|
||||||
1. The browser imports the AES key from the URL.
|
1. The browser imports the AES key from the URL.
|
||||||
2. The browser asks the server for the password using the ID in the URL.
|
2. The browser asks the server for the password using the ID in the URL.
|
||||||
3. The browser decrypts the password from the server using the AES key from the URL.
|
3. The browser decrypts the password from the server using the AES key from the URL.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Setting up PassED can be done with docker compose or from source. As the website relies on the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) it requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). In other words you must setup a reverse proxy for HTTPS, or access the site via `localhost`.
|
Setting up PassED can be done with docker compose or from source. As the website relies on the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) it requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). In other words you must setup a reverse proxy for HTTPS, or access the site via `localhost`.
|
||||||
|
|
||||||
### Docker compose
|
### Docker compose
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
"passed":
|
"passed":
|
||||||
image: "git.1e99.eu/1e99/passed:latest" # You can also use the latest commit hash as the tag to fix your image to a version.
|
image: "git.1e99.eu/1e99/passed:latest"
|
||||||
volumes:
|
volumes:
|
||||||
- "./passed:/etc/passed"
|
- "./passed:/etc/passed"
|
||||||
environment:
|
environment:
|
||||||
|
@ -52,42 +46,30 @@ services:
|
||||||
```
|
```
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
||||||
1. Clonse the source code
|
1. Clonse the source code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.1e99.eu/1e99/passed.git --depth 1
|
git clone https://git.1e99.eu/1e99/passed.git --depth 1
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Ensure that you have go installed, if not follow this [guide](https://go.dev/doc/install).
|
2. Ensure that you have go installed, if not follow this [guide](https://go.dev/doc/install).
|
||||||
3. Build the project.
|
3. Build the project.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -o passed .
|
go build -o passed .
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run the binary.
|
4. Run the binary.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PASSED_STORE_TYPE=dir ./passed
|
PASSED_STORE_TYPE=dir ./passed
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is done using environment variables.
|
Configuration is done using environment variables.
|
||||||
|
|
||||||
- `PASSED_ADDRESS`: The address that PassED should listen on, defaults to `:3000`.
|
- `PASSED_ADDRESS`: The address that PassED should listen on, defaults to `:3000`.
|
||||||
- `PASSED_LOG_REQUESTS`: Should requests be logged, defaults to `true`.
|
- `PASSED_LOG_REQUESTS`: Should requests be logged, defaults to `true`.
|
||||||
- `PASSED_MAX_LENGTH`: Maximum password length in bytes, defaults to `12288` (12 KiB).
|
- `PASSED_MAX_LENGTH`: Maximum password length in KiB, defaults to `12288`.
|
||||||
- `PASSED_STORE_TYPE`: Store type to pick, defaults to `ram`.
|
- `PASSED_STORE_TYPE`: Store type to pick, defaults to `ram`.
|
||||||
- `ram`: Passwords are stored in RAM.
|
- `ram`: Passwords are stored in RAM.
|
||||||
- `dir`: Passwords are stored in a directory. The directory is specified using `PASSED_STORE_DIR_PATH`, which defaults to `passwords`. PassED will create the directory for you.
|
- `dir`: Passwords are stored in a directory. The directory is specified using `PASSED_STORE_DIR_PATH`, which defaults to `passwords`. PassED will **not** create the directory for you.
|
||||||
- `PASSED_STORE_CLEAR_INTERVAL`: Time that should pass between clearing expired passwords in seconds, defaults to `30`.
|
- `PASSED_STORE_CLEAR_INTERVAL`: Time that should pass between clearing expired passwords in seconds, defaults to `30`.
|
||||||
- `PASSED_STATIC_TYPE`: Directory from which static files are served, defaults to `embed`.
|
|
||||||
- `embed`: Static files are served from the files located within the binary. This is recommended.
|
|
||||||
- `dir`: Static files are served from a directory. The directory is specified using `PASSED_STATIC_DIR_PATH`, which defaults to `static`. PassED will not create the directory for you.
|
|
||||||
|
|
||||||
## Translators
|
## Translators
|
||||||
|
|
||||||
- RoryBD
|
- RoryBD
|
||||||
- Nexio
|
- Nexio
|
||||||
|
|
15
build.sh
15
build.sh
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
tag_latest=git.1e99.eu/1e99/passed:latest
|
|
||||||
tag_commit=git.1e99.eu/1e99/passed:$(git rev-parse HEAD)
|
|
||||||
|
|
||||||
# TODO: Figure out other platforms
|
|
||||||
docker image build \
|
|
||||||
--force-rm \
|
|
||||||
--tag $tag_latest \
|
|
||||||
--tag $tag_commit \
|
|
||||||
.
|
|
||||||
|
|
||||||
docker image push $tag_latest
|
|
||||||
docker image push $tag_commit
|
|
37
config/config.go
Normal file
37
config/config.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Env(name string, out any, def string) {
|
||||||
|
raw := os.Getenv(name)
|
||||||
|
if raw == "" {
|
||||||
|
raw = def
|
||||||
|
log.Printf("No \"%s\" provided, defaulting to \"%s\".", name, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := out.(type) {
|
||||||
|
case *int:
|
||||||
|
i, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("\"%s\" is not a number (\"%s\").", name, raw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*value = int(i)
|
||||||
|
|
||||||
|
case *bool:
|
||||||
|
switch raw {
|
||||||
|
case "true", "TRUE", "1":
|
||||||
|
*value = true
|
||||||
|
case "false", "FALSE", "0":
|
||||||
|
*value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case *string:
|
||||||
|
*value = raw
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module git.1e99.eu/1e99/passed
|
module git.1e99.eu/1e99/passed
|
||||||
|
|
||||||
go 1.23.3
|
go 1.23.1
|
||||||
|
|
78
main.go
78
main.go
|
@ -3,13 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.1e99.eu/1e99/passed/config"
|
||||||
"git.1e99.eu/1e99/passed/routes"
|
"git.1e99.eu/1e99/passed/routes"
|
||||||
"git.1e99.eu/1e99/passed/storage"
|
"git.1e99.eu/1e99/passed/storage"
|
||||||
)
|
)
|
||||||
|
@ -23,22 +21,17 @@ func run() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
staticFS, err := newStaticFS()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var address string
|
var address string
|
||||||
var logRequests bool
|
var logRequests bool
|
||||||
var maxPasswordLength int
|
var maxPasswordLength int
|
||||||
env("PASSED_ADDRESS", &address, ":3000")
|
config.Env("PASSED_ADDRESS", &address, ":3000")
|
||||||
env("PASSED_LOG_REQUESTS", &logRequests, "true")
|
config.Env("PASSED_LOG_REQUESTS", &logRequests, "true")
|
||||||
env("PASSED_MAX_LENGTH", &maxPasswordLength, "12288")
|
config.Env("PASSED_MAX_LENGTH", &maxPasswordLength, "12288")
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
handler := http.Handler(mux)
|
handler := http.Handler(mux)
|
||||||
|
|
||||||
mux.Handle("GET /", http.FileServerFS(staticFS))
|
mux.Handle("GET /", routes.ServeFiles(embedFS, "static"))
|
||||||
mux.Handle("POST /api/password", routes.CreatePassword(store, maxPasswordLength))
|
mux.Handle("POST /api/password", routes.CreatePassword(store, maxPasswordLength))
|
||||||
mux.Handle("GET /api/password/{id}", routes.GetPassword(store))
|
mux.Handle("GET /api/password/{id}", routes.GetPassword(store))
|
||||||
mux.Handle("HEAD /api/password/{id}", routes.HasPassword(store))
|
mux.Handle("HEAD /api/password/{id}", routes.HasPassword(store))
|
||||||
|
@ -56,43 +49,18 @@ func run() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStaticFS() (sfs fs.FS, err error) {
|
|
||||||
var fsType string
|
|
||||||
env("PASSED_STATIC_TYPE", &fsType, "embed")
|
|
||||||
|
|
||||||
switch fsType {
|
|
||||||
case "embed":
|
|
||||||
sfs, err = fs.Sub(embedFS, "static")
|
|
||||||
return
|
|
||||||
case "dir", "directory":
|
|
||||||
var path string
|
|
||||||
env("PASSED_STATIC_DIR_PATH", &path, "static")
|
|
||||||
|
|
||||||
sfs = os.DirFS(path)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
err = errors.New("unkown fs type")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStore() (store storage.Store, err error) {
|
func newStore() (store storage.Store, err error) {
|
||||||
var storeType string
|
var storeType string
|
||||||
var clearInterval int
|
var clearInterval int
|
||||||
env("PASSED_STORE_TYPE", &storeType, "ram")
|
config.Env("PASSED_STORE_TYPE", &storeType, "ram")
|
||||||
env("PASSED_STORE_CLEAR_INTERVAL", &clearInterval, "30")
|
config.Env("PASSED_STORE_CLEAR_INTERVAL", &clearInterval, "30")
|
||||||
|
|
||||||
switch storeType {
|
switch storeType {
|
||||||
case "ram":
|
case "ram":
|
||||||
store = storage.NewRamStore()
|
store = storage.NewRamStore()
|
||||||
case "dir", "directory":
|
case "dir", "directory":
|
||||||
var path string
|
var path string
|
||||||
env("PASSED_STORE_DIR_PATH", &path, "passwords")
|
config.Env("PASSED_STORE_DIR_PATH", &path, "passwords")
|
||||||
|
|
||||||
err = os.MkdirAll(path, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
store = storage.NewDirStore(path)
|
store = storage.NewDirStore(path)
|
||||||
default:
|
default:
|
||||||
|
@ -124,33 +92,3 @@ func main() {
|
||||||
log.Fatalf("%s", err)
|
log.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func env(name string, out any, def string) {
|
|
||||||
raw := os.Getenv(name)
|
|
||||||
if raw == "" {
|
|
||||||
raw = def
|
|
||||||
log.Printf("No \"%s\" provided, defaulting to \"%s\".", name, def)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch value := out.(type) {
|
|
||||||
case *int:
|
|
||||||
i, err := strconv.ParseInt(raw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("\"%s\" is not a number (\"%s\").", name, raw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*value = int(i)
|
|
||||||
|
|
||||||
case *bool:
|
|
||||||
switch raw {
|
|
||||||
case "true", "TRUE", "1":
|
|
||||||
*value = true
|
|
||||||
case "false", "FALSE", "0":
|
|
||||||
*value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
case *string:
|
|
||||||
*value = raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
15
routes/serve_files.go
Normal file
15
routes/serve_files.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServeFiles(fileSystem fs.FS, pathInFs string) http.Handler {
|
||||||
|
newFs, err := fs.Sub(fileSystem, pathInFs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.FileServerFS(newFs)
|
||||||
|
}
|
|
@ -30,12 +30,12 @@
|
||||||
<details class="dropdown">
|
<details class="dropdown">
|
||||||
<summary t="language"></summary>
|
<summary t="language"></summary>
|
||||||
<ul dir="rtl">
|
<ul dir="rtl">
|
||||||
<li class="select-language" data-lang="de">Deutsch</li>
|
<li t="language-de" class="select-language" data-lang="de"></li>
|
||||||
<li class="select-language" data-lang="en">English</li>
|
<li t="language-en" class="select-language" data-lang="en"></li>
|
||||||
<li class="select-language" data-lang="es">Español</li>
|
<li t="language-es" class="select-language" data-lang="es"></li>
|
||||||
<li class="select-language" data-lang="fr">Français</li>
|
<li t="language-fr" class="select-language" data-lang="fr"></li>
|
||||||
<li class="select-language" data-lang="nl">Nederlands</li>
|
<li t="language-nl" class="select-language" data-lang="nl"></li>
|
||||||
<li class="select-language" data-lang="ro">Română</li>
|
<li t="language-ro" class="select-language" data-lang="ro"></li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
@ -113,17 +113,5 @@
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog id="error">
|
|
||||||
<article>
|
|
||||||
<header t="error"></header>
|
|
||||||
|
|
||||||
<textarea readonly id="error-message"></textarea>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<button t="close" id="error-close"></button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
</dialog>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,17 +1,4 @@
|
||||||
function initErrorDialog() {
|
function initEnterPassword() {
|
||||||
const dialog = document.querySelector("dialog#error");
|
|
||||||
const message = document.querySelector("textarea#error-message");
|
|
||||||
const close = document.querySelector("button#error-close");
|
|
||||||
|
|
||||||
close.addEventListener("click", () => dialog.close());
|
|
||||||
|
|
||||||
return (err) => {
|
|
||||||
message.value = err.toString();
|
|
||||||
dialog.showModal();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function initEnterPassword(errorDialog) {
|
|
||||||
const form = document.querySelector("form#enter-password");
|
const form = document.querySelector("form#enter-password");
|
||||||
const fieldset = document.querySelector("fieldset#enter-password");
|
const fieldset = document.querySelector("fieldset#enter-password");
|
||||||
const submit = document.querySelector("button#enter-password");
|
const submit = document.querySelector("button#enter-password");
|
||||||
|
@ -41,8 +28,6 @@ function initEnterPassword(errorDialog) {
|
||||||
|
|
||||||
link.value = url.toString();
|
link.value = url.toString();
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
} catch (error) {
|
|
||||||
errorDialog(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
fieldset.disabled = false;
|
fieldset.disabled = false;
|
||||||
submit.ariaBusy = "false";
|
submit.ariaBusy = "false";
|
||||||
|
@ -65,7 +50,7 @@ function initEnterPassword(errorDialog) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmViewPassword(errorDialog) {
|
async function confirmViewPassword() {
|
||||||
const dialog = document.querySelector("dialog#confirm");
|
const dialog = document.querySelector("dialog#confirm");
|
||||||
const close = document.querySelector("button#confirm-close");
|
const close = document.querySelector("button#confirm-close");
|
||||||
const ok = document.querySelector("button#confirm-ok");
|
const ok = document.querySelector("button#confirm-ok");
|
||||||
|
@ -104,8 +89,6 @@ async function confirmViewPassword(errorDialog) {
|
||||||
const password = await decryptPassword(encryptedPassword, key, iv);
|
const password = await decryptPassword(encryptedPassword, key, iv);
|
||||||
|
|
||||||
viewPassword(password);
|
viewPassword(password);
|
||||||
} catch (error) {
|
|
||||||
errorDialog(error);
|
|
||||||
} finally {
|
} finally {
|
||||||
close.disabled = false;
|
close.disabled = false;
|
||||||
ok.disabled = false;
|
ok.disabled = false;
|
||||||
|
@ -130,7 +113,6 @@ async function viewPassword(password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
const errorDialog = initErrorDialog();
|
initEnterPassword();
|
||||||
initEnterPassword(errorDialog);
|
confirmViewPassword();
|
||||||
confirmViewPassword(errorDialog);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Quellcode",
|
"source-code": "Quellcode",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
"language-en": "Englisch",
|
||||||
|
"language-de": "Deutsch",
|
||||||
|
"language-es": "Spanisch",
|
||||||
|
"language-fr": "Französich",
|
||||||
|
"language-nl": "Niederländisch",
|
||||||
"enter-password": "Passwort eingeben",
|
"enter-password": "Passwort eingeben",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"expires-in": "Ablaufzeit",
|
"expires-in": "Ablaufzeit",
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Source",
|
"source-code": "Source",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"language-en": "English",
|
||||||
|
"language-de": "German",
|
||||||
|
"language-es": "Spanish",
|
||||||
|
"language-fr": "French",
|
||||||
|
"language-nl": "Dutch",
|
||||||
"enter-password": "Enter Password",
|
"enter-password": "Enter Password",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"expires-in": "Expires in",
|
"expires-in": "Expires in",
|
||||||
|
@ -16,7 +21,6 @@
|
||||||
"not-found-reason": "The password you requested may have expired, been viewed before or never even existed in the first place.",
|
"not-found-reason": "The password you requested may have expired, been viewed before or never even existed in the first place.",
|
||||||
"reveal-password": "Reveal Password",
|
"reveal-password": "Reveal Password",
|
||||||
"reveal-password-once": "You may only reveal the password once",
|
"reveal-password-once": "You may only reveal the password once",
|
||||||
"error": "Error",
|
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"ok": "OK"
|
"ok": "OK"
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Fuente",
|
"source-code": "Fuente",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
"language-en": "Inglés",
|
||||||
|
"language-de": "Alemán",
|
||||||
|
"language-es": "Español",
|
||||||
|
"language-fr": "Francés",
|
||||||
"enter-password": "Introducir Contraseña",
|
"enter-password": "Introducir Contraseña",
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"expires-in": "Expira en",
|
"expires-in": "Expira en",
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Source",
|
"source-code": "Source",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
|
"language-en": "Anglais",
|
||||||
|
"language-de": "Allemand",
|
||||||
|
"language-es": "Espagnol",
|
||||||
|
"language-fr": "Français",
|
||||||
"enter-password": "Entrer le mot de passe",
|
"enter-password": "Entrer le mot de passe",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"expires-in": "Expire dans",
|
"expires-in": "Expire dans",
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Bron",
|
"source-code": "Bron",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
|
"language-en": "Engels",
|
||||||
|
"language-de": "Duits",
|
||||||
|
"language-es": "Spaans",
|
||||||
|
"language-fr": "Frans",
|
||||||
"enter-password": "Wachtwoord invoeren",
|
"enter-password": "Wachtwoord invoeren",
|
||||||
"password": "Wachtwoord",
|
"password": "Wachtwoord",
|
||||||
"expires-in": "Expires in",
|
"expires-in": "Expires in",
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
"title": "PassED",
|
"title": "PassED",
|
||||||
"source-code": "Sursă",
|
"source-code": "Sursă",
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
|
"language-en": "Engleză",
|
||||||
|
"language-de": "Germană",
|
||||||
|
"language-ro": "Română",
|
||||||
"enter-password": "Introduceți parolă",
|
"enter-password": "Introduceți parolă",
|
||||||
"password": "Parolă",
|
"password": "Parolă",
|
||||||
"expires-in": "Expiră în",
|
"expires-in": "Expiră în",
|
||||||
|
@ -11,7 +14,7 @@
|
||||||
"expires-in.1-week": "O săptămână",
|
"expires-in.1-week": "O săptămână",
|
||||||
"expires-in.2-weeks": "2 săptămâni",
|
"expires-in.2-weeks": "2 săptămâni",
|
||||||
"generate-link": "Generați link",
|
"generate-link": "Generați link",
|
||||||
"share-link": "Distribuiți link",
|
"share-link": "Distribuți link",
|
||||||
"not-found": "Parola nu poate fi găsită",
|
"not-found": "Parola nu poate fi găsită",
|
||||||
"not-found-reason": "Parola cerută este posibil să fi expirat, fost folosită, sau să nu existe.",
|
"not-found-reason": "Parola cerută este posibil să fi expirat, fost folosită, sau să nu existe.",
|
||||||
"reveal-password": "Dezvăluiți parola",
|
"reveal-password": "Dezvăluiți parola",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
@ -50,6 +51,7 @@ func (store *dir) Create(password []byte, expiresAt time.Time) (string, error) {
|
||||||
|
|
||||||
err = gob.NewEncoder(file).Encode(&entry)
|
err = gob.NewEncoder(file).Encode(&entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("%s", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue