Compare commits
11 Commits
message-ca
...
http-clipb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb386af58 | ||
|
|
b105ed6727 | ||
|
|
965110b2c3 | ||
|
|
c8ac104043 | ||
|
|
e39498702d | ||
|
|
9b97067b10 | ||
|
|
5244e0be14 | ||
|
|
6eb25f68ac | ||
|
|
efe7c3fa70 | ||
|
|
ce4b2ae9a0 | ||
|
|
4eb7dc563c |
@@ -555,8 +555,8 @@ func parseUsers(usersRaw []string) ([]*user.User, error) {
|
|||||||
role := user.Role(strings.TrimSpace(parts[2]))
|
role := user.Role(strings.TrimSpace(parts[2]))
|
||||||
if !user.AllowedUsername(username) {
|
if !user.AllowedUsername(username) {
|
||||||
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
|
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
|
||||||
} else if err := user.ValidPasswordHash(passwordHash); err != nil {
|
} else if err := user.ValidPasswordHash(passwordHash, user.DefaultUserPasswordBcryptCost); err != nil {
|
||||||
return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error())
|
return nil, fmt.Errorf("invalid auth-users: %s, password hash invalid, %s", userLine, err.Error())
|
||||||
} else if !user.AllowedRole(role) {
|
} else if !user.AllowedRole(role) {
|
||||||
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ func TestParseUsers_Success(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "single user",
|
name: "single user",
|
||||||
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||||
expected: []*user.User{
|
expected: []*user.User{
|
||||||
{
|
{
|
||||||
Name: "alice",
|
Name: "alice",
|
||||||
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||||
Role: user.RoleUser,
|
Role: user.RoleUser,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
},
|
},
|
||||||
@@ -39,19 +39,19 @@ func TestParseUsers_Success(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "multiple users with different roles",
|
name: "multiple users with different roles",
|
||||||
input: []string{
|
input: []string{
|
||||||
"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user",
|
"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user",
|
||||||
"bob:$2b$10$abcdefghijklmnopqrstuvwxyz:admin",
|
"bob:$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq:admin",
|
||||||
},
|
},
|
||||||
expected: []*user.User{
|
expected: []*user.User{
|
||||||
{
|
{
|
||||||
Name: "alice",
|
Name: "alice",
|
||||||
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||||
Role: user.RoleUser,
|
Role: user.RoleUser,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "bob",
|
Name: "bob",
|
||||||
Hash: "$2b$10$abcdefghijklmnopqrstuvwxyz",
|
Hash: "$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq",
|
||||||
Role: user.RoleAdmin,
|
Role: user.RoleAdmin,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
},
|
},
|
||||||
@@ -64,11 +64,11 @@ func TestParseUsers_Success(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user with special characters in name",
|
name: "user with special characters in name",
|
||||||
input: []string{"alice.test+123@example.com:$2y$10$abcdefghijklmnopqrstuvwxyz:user"},
|
input: []string{"alice.test+123@example.com:$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe:user"},
|
||||||
expected: []*user.User{
|
expected: []*user.User{
|
||||||
{
|
{
|
||||||
Name: "alice.test+123@example.com",
|
Name: "alice.test+123@example.com",
|
||||||
Hash: "$2y$10$abcdefghijklmnopqrstuvwxyz",
|
Hash: "$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe",
|
||||||
Role: user.RoleUser,
|
Role: user.RoleUser,
|
||||||
Provisioned: true,
|
Provisioned: true,
|
||||||
},
|
},
|
||||||
@@ -110,23 +110,23 @@ func TestParseUsers_Errors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid username",
|
name: "invalid username",
|
||||||
input: []string{"alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
input: []string{"alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||||
error: "invalid auth-users: alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid",
|
error: "invalid auth-users: alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid password hash - wrong prefix",
|
name: "invalid password hash - wrong prefix",
|
||||||
input: []string{"alice:plaintext:user"},
|
input: []string{"alice:plaintext:user"},
|
||||||
error: "invalid auth-users: alice:plaintext:user, password hash but be a bcrypt hash, use 'ntfy user hash' to generate",
|
error: "invalid auth-users: alice:plaintext:user, password hash invalid, password hash must be a bcrypt hash, use 'ntfy user hash' to generate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid role",
|
name: "invalid role",
|
||||||
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid"},
|
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid"},
|
||||||
error: "invalid auth-users: alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
|
error: "invalid auth-users: alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty username",
|
name: "empty username",
|
||||||
input: []string{":$2a$10$abcdefghijklmnopqrstuvwxyz:user"},
|
input: []string{":$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||||
error: "invalid auth-users: :$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid",
|
error: "invalid auth-users: :$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ using Docker Compose (i.e. `docker-compose.yml`):
|
|||||||
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
|
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
|
||||||
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
|
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
|
||||||
NTFY_AUTH_DEFAULT_ACCESS: deny-all
|
NTFY_AUTH_DEFAULT_ACCESS: deny-all
|
||||||
NTFY_AUTH_USERS: 'phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin'
|
NTFY_AUTH_USERS: 'phil:$$2a$$10$$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' # Must escape '$' as '$$'
|
||||||
NTFY_BEHIND_PROXY: true
|
NTFY_BEHIND_PROXY: true
|
||||||
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
|
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
|
||||||
NTFY_ENABLE_LOGIN: true
|
NTFY_ENABLE_LOGIN: true
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||||||
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
|
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
|
||||||
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
|
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
|
||||||
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service.
|
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service.
|
||||||
|
- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform
|
||||||
|
- [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy
|
||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
|
|||||||
@@ -3679,13 +3679,13 @@ authParam = base64_raw(authHeader) // -> QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM0
|
|||||||
The following command will generate the appropriate value for you on *nix systems:
|
The following command will generate the appropriate value for you on *nix systems:
|
||||||
|
|
||||||
```
|
```
|
||||||
echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '='
|
echo -n "Basic `echo -n 'testuser:fakepassword' | base64 -w0`" | base64 -w0 | tr -d '='
|
||||||
```
|
```
|
||||||
|
|
||||||
For access tokens, you can use this instead:
|
For access tokens, you can use this instead:
|
||||||
|
|
||||||
```
|
```
|
||||||
echo -n "Bearer faketoken" | base64 | tr -d '='
|
echo -n "Bearer faketoken" | base64 -w0 | tr -d '='
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced features
|
## Advanced features
|
||||||
|
|||||||
@@ -1475,6 +1475,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
|
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
|
||||||
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
|
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
|
||||||
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
|
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
|
||||||
|
* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting)
|
||||||
|
|
||||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||||
|
|
||||||
|
|||||||
@@ -1066,7 +1066,7 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
|
|||||||
var err error = nil
|
var err error = nil
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = password
|
hash = password
|
||||||
if err := ValidPasswordHash(hash); err != nil {
|
if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1434,7 +1434,7 @@ func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed
|
|||||||
var err error
|
var err error
|
||||||
if hashed {
|
if hashed {
|
||||||
hash = password
|
hash = password
|
||||||
if err := ValidPasswordHash(hash); err != nil {
|
if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1162,7 +1162,7 @@ func TestManager_WithProvisionedUsers(t *testing.T) {
|
|||||||
// Re-open the DB (second app start)
|
// Re-open the DB (second app start)
|
||||||
require.Nil(t, a.db.Close())
|
require.Nil(t, a.db.Close())
|
||||||
conf.Users = []*User{
|
conf.Users = []*User{
|
||||||
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
{Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||||
}
|
}
|
||||||
conf.Access = map[string][]*Grant{
|
conf.Access = map[string][]*Grant{
|
||||||
"philuser": {
|
"philuser": {
|
||||||
@@ -1292,7 +1292,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
|
|||||||
// Re-open the DB (second app start)
|
// Re-open the DB (second app start)
|
||||||
require.Nil(t, a.db.Close())
|
require.Nil(t, a.db.Close())
|
||||||
conf.Users = []*User{
|
conf.Users = []*User{
|
||||||
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
{Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
|
||||||
}
|
}
|
||||||
conf.Access = map[string][]*Grant{
|
conf.Access = map[string][]*Grant{
|
||||||
"philuser": {
|
"philuser": {
|
||||||
@@ -1308,7 +1308,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
|
|||||||
require.Len(t, users, 2)
|
require.Len(t, users, 2)
|
||||||
require.Equal(t, "philuser", users[0].Name)
|
require.Equal(t, "philuser", users[0].Name)
|
||||||
require.Equal(t, RoleUser, users[0].Role)
|
require.Equal(t, RoleUser, users[0].Role)
|
||||||
require.Equal(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash)
|
require.Equal(t, "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash)
|
||||||
require.True(t, users[0].Provisioned) // Updated to provisioned!
|
require.True(t, users[0].Provisioned) // Updated to provisioned!
|
||||||
|
|
||||||
grants, err = a.Grants("philuser")
|
grants, err = a.Grants("philuser")
|
||||||
|
|||||||
@@ -249,7 +249,8 @@ var (
|
|||||||
ErrInvalidArgument = errors.New("invalid argument")
|
ErrInvalidArgument = errors.New("invalid argument")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
ErrUserExists = errors.New("user already exists")
|
ErrUserExists = errors.New("user already exists")
|
||||||
ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate")
|
ErrPasswordHashInvalid = errors.New("password hash must be a bcrypt hash, use 'ntfy user hash' to generate")
|
||||||
|
ErrPasswordHashWeak = errors.New("password hash too weak, use 'ntfy user hash' to generate")
|
||||||
ErrTierNotFound = errors.New("tier not found")
|
ErrTierNotFound = errors.New("tier not found")
|
||||||
ErrTokenNotFound = errors.New("token not found")
|
ErrTokenNotFound = errors.New("token not found")
|
||||||
ErrPhoneNumberNotFound = errors.New("phone number not found")
|
ErrPhoneNumberNotFound = errors.New("phone number not found")
|
||||||
|
|||||||
@@ -41,10 +41,16 @@ func AllowedTier(tier string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidPasswordHash checks if the given password hash is a valid bcrypt hash
|
// ValidPasswordHash checks if the given password hash is a valid bcrypt hash
|
||||||
func ValidPasswordHash(hash string) error {
|
func ValidPasswordHash(hash string, minCost int) error {
|
||||||
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
|
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
|
||||||
return ErrPasswordHashInvalid
|
return ErrPasswordHashInvalid
|
||||||
}
|
}
|
||||||
|
cost, err := bcrypt.Cost([]byte(hash))
|
||||||
|
if err != nil { // Check if the hash is valid (length, format, etc.)
|
||||||
|
return err
|
||||||
|
} else if cost < minCost {
|
||||||
|
return ErrPasswordHashWeak
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ export const maybeWithBearerAuth = (headers, token) => {
|
|||||||
return headers;
|
return headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
|
export const withBasicAuth = (headers, username, password) => ({
|
||||||
|
...headers,
|
||||||
|
Authorization: basicAuth(username, password)
|
||||||
|
});
|
||||||
|
|
||||||
export const maybeWithAuth = (headers, user) => {
|
export const maybeWithAuth = (headers, user) => {
|
||||||
if (user?.password) {
|
if (user?.password) {
|
||||||
@@ -139,7 +142,7 @@ export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-");
|
|||||||
export const formatShortDateTime = (timestamp, language) =>
|
export const formatShortDateTime = (timestamp, language) =>
|
||||||
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
|
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
timeStyle: "short",
|
timeStyle: "short"
|
||||||
}).format(new Date(timestamp * 1000));
|
}).format(new Date(timestamp * 1000));
|
||||||
|
|
||||||
export const formatShortDate = (timestamp, language) =>
|
export const formatShortDate = (timestamp, language) =>
|
||||||
@@ -178,32 +181,32 @@ export const openUrl = (url) => {
|
|||||||
export const sounds = {
|
export const sounds = {
|
||||||
ding: {
|
ding: {
|
||||||
file: ding,
|
file: ding,
|
||||||
label: "Ding",
|
label: "Ding"
|
||||||
},
|
},
|
||||||
juntos: {
|
juntos: {
|
||||||
file: juntos,
|
file: juntos,
|
||||||
label: "Juntos",
|
label: "Juntos"
|
||||||
},
|
},
|
||||||
pristine: {
|
pristine: {
|
||||||
file: pristine,
|
file: pristine,
|
||||||
label: "Pristine",
|
label: "Pristine"
|
||||||
},
|
},
|
||||||
dadum: {
|
dadum: {
|
||||||
file: dadum,
|
file: dadum,
|
||||||
label: "Dadum",
|
label: "Dadum"
|
||||||
},
|
},
|
||||||
pop: {
|
pop: {
|
||||||
file: pop,
|
file: pop,
|
||||||
label: "Pop",
|
label: "Pop"
|
||||||
},
|
},
|
||||||
"pop-swoosh": {
|
"pop-swoosh": {
|
||||||
file: popSwoosh,
|
file: popSwoosh,
|
||||||
label: "Pop swoosh",
|
label: "Pop swoosh"
|
||||||
},
|
},
|
||||||
beep: {
|
beep: {
|
||||||
file: beep,
|
file: beep,
|
||||||
label: "Beep",
|
label: "Beep"
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const playSound = async (id) => {
|
export const playSound = async (id) => {
|
||||||
@@ -216,7 +219,7 @@ export const playSound = async (id) => {
|
|||||||
export async function* fetchLinesIterator(fileURL, headers) {
|
export async function* fetchLinesIterator(fileURL, headers) {
|
||||||
const utf8Decoder = new TextDecoder("utf-8");
|
const utf8Decoder = new TextDecoder("utf-8");
|
||||||
const response = await fetch(fileURL, {
|
const response = await fetch(fileURL, {
|
||||||
headers,
|
headers
|
||||||
});
|
});
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
let { value: chunk, done: readerDone } = await reader.read();
|
let { value: chunk, done: readerDone } = await reader.read();
|
||||||
@@ -225,7 +228,7 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
|||||||
const re = /\n|\r|\r\n/gm;
|
const re = /\n|\r|\r\n/gm;
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
for (;;) {
|
for (; ;) {
|
||||||
const result = re.exec(chunk);
|
const result = re.exec(chunk);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (readerDone) {
|
if (readerDone) {
|
||||||
@@ -270,3 +273,21 @@ export const urlB64ToUint8Array = (base64String) => {
|
|||||||
}
|
}
|
||||||
return outputArray;
|
return outputArray;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const copyToClipboard = (text) => {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute("readonly", ""); // Avoid mobile keyboards from popping up
|
||||||
|
textarea.style.position = "fixed"; // Avoid scroll jump
|
||||||
|
textarea.style.left = "-9999px";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
|||||||
import { ContentCopy, Public } from "@mui/icons-material";
|
import { ContentCopy, Public } from "@mui/icons-material";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
|
import { copyToClipboard, formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
|
||||||
import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
|
import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
|
||||||
import { Pref, PrefGroup } from "./Pref";
|
import { Pref, PrefGroup } from "./Pref";
|
||||||
import db from "../app/db";
|
import db from "../app/db";
|
||||||
@@ -370,7 +370,7 @@ const PhoneNumbers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = (phoneNumber) => {
|
const handleCopy = (phoneNumber) => {
|
||||||
navigator.clipboard.writeText(phoneNumber);
|
copyToClipboard(phoneNumber);
|
||||||
setSnackOpen(true);
|
setSnackOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -841,7 +841,7 @@ const TokensTable = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = async (token) => {
|
const handleCopy = async (token) => {
|
||||||
await navigator.clipboard.writeText(token);
|
copyToClipboard(token);
|
||||||
setSnackOpen(true);
|
setSnackOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
|||||||
import StackTrace from "stacktrace-js";
|
import StackTrace from "stacktrace-js";
|
||||||
import { CircularProgress, Link, Button } from "@mui/material";
|
import { CircularProgress, Link, Button } from "@mui/material";
|
||||||
import { Trans, withTranslation } from "react-i18next";
|
import { Trans, withTranslation } from "react-i18next";
|
||||||
|
import { copyToClipboard } from "../app/utils";
|
||||||
|
|
||||||
class ErrorBoundaryImpl extends React.Component {
|
class ErrorBoundaryImpl extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -64,7 +65,7 @@ class ErrorBoundaryImpl extends React.Component {
|
|||||||
stack += `${this.state.niceStack}\n\n`;
|
stack += `${this.state.niceStack}\n\n`;
|
||||||
}
|
}
|
||||||
stack += `${this.state.originalStack}\n`;
|
stack += `${this.state.originalStack}\n`;
|
||||||
navigator.clipboard.writeText(stack);
|
copyToClipboard(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUnsupportedIndexedDB() {
|
renderUnsupportedIndexedDB() {
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import { Trans, useTranslation } from "react-i18next";
|
|||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { useRemark } from "react-remark";
|
import { useRemark } from "react-remark";
|
||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags
|
||||||
|
} from "../app/utils";
|
||||||
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
||||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
@@ -239,7 +242,7 @@ const NotificationItem = (props) => {
|
|||||||
await subscriptionManager.markNotificationRead(notification.id);
|
await subscriptionManager.markNotificationRead(notification.id);
|
||||||
};
|
};
|
||||||
const handleCopy = (s) => {
|
const handleCopy = (s) => {
|
||||||
navigator.clipboard.writeText(s);
|
copyToClipboard(s);
|
||||||
props.onShowSnack();
|
props.onShowSnack();
|
||||||
};
|
};
|
||||||
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
||||||
|
|||||||
Reference in New Issue
Block a user