Compare commits
10 commits
7f07e40122
...
f6192f6118
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f6192f6118 | ||
![]() |
027d9f573c | ||
![]() |
7df811beb4 | ||
![]() |
d1d5f93486 | ||
![]() |
71e28f94ca | ||
![]() |
972e941da3 | ||
![]() |
6855166afc | ||
![]() |
d5d6d53946 | ||
![]() |
049402203b | ||
![]() |
7171af3687 |
17 changed files with 150 additions and 104 deletions
|
@ -1,5 +1,4 @@
|
||||||
when:
|
when:
|
||||||
- event: "push"
|
|
||||||
- event: "manual"
|
- event: "manual"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23.1 AS builder
|
FROM golang:1.23.3 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,4 +1,5 @@
|
||||||
# PassED
|
# PassED
|
||||||
|
|
||||||
[](https://discord.gg/NuGxJKtDKS)
|
[](https://discord.gg/NuGxJKtDKS)
|
||||||
[](https://passed.1e99.eu/)
|
[](https://passed.1e99.eu/)
|
||||||
[](https://go.dev/)
|
[](https://go.dev/)
|
||||||
|
@ -16,25 +17,30 @@ 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"
|
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.
|
||||||
volumes:
|
volumes:
|
||||||
- "./passed:/etc/passed"
|
- "./passed:/etc/passed"
|
||||||
environment:
|
environment:
|
||||||
|
@ -46,30 +52,42 @@ 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 KiB, defaults to `12288`.
|
- `PASSED_MAX_LENGTH`: Maximum password length in bytes, defaults to `12288` (12 KiB).
|
||||||
- `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 **not** 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 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
Executable file
15
build.sh
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/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
|
|
@ -1,37 +0,0 @@
|
||||||
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.1
|
go 1.23.3
|
||||||
|
|
78
main.go
78
main.go
|
@ -3,11 +3,13 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -21,17 +23,22 @@ 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
|
||||||
config.Env("PASSED_ADDRESS", &address, ":3000")
|
env("PASSED_ADDRESS", &address, ":3000")
|
||||||
config.Env("PASSED_LOG_REQUESTS", &logRequests, "true")
|
env("PASSED_LOG_REQUESTS", &logRequests, "true")
|
||||||
config.Env("PASSED_MAX_LENGTH", &maxPasswordLength, "12288")
|
env("PASSED_MAX_LENGTH", &maxPasswordLength, "12288")
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
handler := http.Handler(mux)
|
handler := http.Handler(mux)
|
||||||
|
|
||||||
mux.Handle("GET /", routes.ServeFiles(embedFS, "static"))
|
mux.Handle("GET /", http.FileServerFS(staticFS))
|
||||||
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))
|
||||||
|
@ -49,18 +56,43 @@ 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
|
||||||
config.Env("PASSED_STORE_TYPE", &storeType, "ram")
|
env("PASSED_STORE_TYPE", &storeType, "ram")
|
||||||
config.Env("PASSED_STORE_CLEAR_INTERVAL", &clearInterval, "30")
|
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
|
||||||
config.Env("PASSED_STORE_DIR_PATH", &path, "passwords")
|
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:
|
||||||
|
@ -92,3 +124,33 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
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 t="language-de" class="select-language" data-lang="de"></li>
|
<li class="select-language" data-lang="de">Deutsch</li>
|
||||||
<li t="language-en" class="select-language" data-lang="en"></li>
|
<li class="select-language" data-lang="en">English</li>
|
||||||
<li t="language-es" class="select-language" data-lang="es"></li>
|
<li class="select-language" data-lang="es">Español</li>
|
||||||
<li t="language-fr" class="select-language" data-lang="fr"></li>
|
<li class="select-language" data-lang="fr">Français</li>
|
||||||
<li t="language-nl" class="select-language" data-lang="nl"></li>
|
<li class="select-language" data-lang="nl">Nederlands</li>
|
||||||
<li t="language-ro" class="select-language" data-lang="ro"></li>
|
<li class="select-language" data-lang="ro">Română</li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
@ -113,5 +113,17 @@
|
||||||
</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,4 +1,17 @@
|
||||||
function initEnterPassword() {
|
function initErrorDialog() {
|
||||||
|
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");
|
||||||
|
@ -28,6 +41,8 @@ function initEnterPassword() {
|
||||||
|
|
||||||
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";
|
||||||
|
@ -50,7 +65,7 @@ function initEnterPassword() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmViewPassword() {
|
async function confirmViewPassword(errorDialog) {
|
||||||
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");
|
||||||
|
@ -89,6 +104,8 @@ async function confirmViewPassword() {
|
||||||
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;
|
||||||
|
@ -113,6 +130,7 @@ async function viewPassword(password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
initEnterPassword();
|
const errorDialog = initErrorDialog();
|
||||||
confirmViewPassword();
|
initEnterPassword(errorDialog);
|
||||||
|
confirmViewPassword(errorDialog);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,6 @@
|
||||||
"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,11 +2,6 @@
|
||||||
"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",
|
||||||
|
@ -21,6 +16,7 @@
|
||||||
"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,10 +2,6 @@
|
||||||
"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,10 +2,6 @@
|
||||||
"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,10 +2,6 @@
|
||||||
"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,9 +2,6 @@
|
||||||
"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",
|
||||||
|
@ -14,7 +11,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": "Distribuți link",
|
"share-link": "Distribuiț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,7 +2,6 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
@ -51,7 +50,6 @@ 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