mirror of
https://github.com/rclone/rclone.git
synced 2025-12-06 00:03:32 +00:00
Compare commits
3 Commits
copilot/fi
...
jotta-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f838c2a2f2 | ||
|
|
49103c7348 | ||
|
|
697874e399 |
@@ -17,7 +17,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -60,7 +60,7 @@ const (
|
|||||||
configVersion = 1
|
configVersion = 1
|
||||||
|
|
||||||
defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
|
defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
|
||||||
defaultClientID = "jottacli"
|
defaultClientID = "jottacli" // Identified as "Jottacloud CLI" in "My logged in devices"
|
||||||
|
|
||||||
legacyTokenURL = "https://api.jottacloud.com/auth/v1/token"
|
legacyTokenURL = "https://api.jottacloud.com/auth/v1/token"
|
||||||
legacyRegisterURL = "https://api.jottacloud.com/auth/v1/register"
|
legacyRegisterURL = "https://api.jottacloud.com/auth/v1/register"
|
||||||
@@ -69,27 +69,29 @@ const (
|
|||||||
legacyConfigVersion = 0
|
legacyConfigVersion = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
func getWhitelabelServices() map[string]struct {
|
type service struct {
|
||||||
name, domain, realm, clientID string
|
key string
|
||||||
scopes []string
|
name string
|
||||||
} {
|
domain string
|
||||||
return map[string]struct {
|
realm string
|
||||||
name string
|
clientID string
|
||||||
domain string
|
scopes []string
|
||||||
realm string
|
}
|
||||||
clientID string
|
|
||||||
scopes []string
|
func getServices() []service {
|
||||||
}{
|
return []service{
|
||||||
"telia_se": {"Telia Cloud (Sweden)", "cloud-auth.telia.se", "telia_se", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"jottacloud", "Jottacloud", "id.jottacloud.com", "jottacloud", "desktop", []string{"openid", "jotta-default", "offline_access"}}, // Chose client id "desktop" here, will be identified as "Jottacloud for Desktop" in "My logged in devices", but could have used "jottacli" here as well.
|
||||||
"telia_no": {"Telia Sky (Norway)", "sky-auth.telia.no", "get", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"elkjop", "Elkjøp Cloud (Norway)", "cloud.elkjop.no", "elkjop", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"tele2": {"Tele2 Cloud (Sweden)", "mittcloud-auth.tele2.se", "comhem", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"elgiganten_dk", "Elgiganten Cloud (Denmark)", "cloud.elgiganten.dk", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"onlime": {"Onlime (Denmark)", "cloud-auth.onlime.dk", "onlime_wl", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"elgiganten_se", "Elgiganten Cloud (Sweden)", "cloud.elgiganten.se", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"elkjop": {"Elkjøp Cloud (Norway)", "cloud.elkjop.no", "elkjop", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"elko", "ELKO Cloud (Iceland)", "cloud.elko.is", "elko", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"elgiganten_se": {"Elgiganten Cloud (Sweden)", "cloud.elgiganten.se", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"gigantti", "Gigantti Cloud (Finland)", "cloud.gigantti.fi", "gigantti", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"elgiganten_dk": {"Elgiganten Cloud (Denmark)", "cloud.elgiganten.dk", "elgiganten", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"telia_se", "Telia Cloud (Sweden)", "cloud-auth.telia.se", "telia_se", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"gigantti": {"Gigantti Cloud (Finland)", "cloud.gigantti.fi", "gigantti", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"telia_no", "Telia Sky (Norway)", "sky-auth.telia.no", "get", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"elko": {"ELKO Cloud (Iceland)", "cloud.elko.is", "elko", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
{"tele2", "Tele2 Cloud (Sweden)", "mittcloud-auth.tele2.se", "comhem", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
"letsgo": {"Let's Go Cloud (Germany)", "letsgo.jotta.cloud", "letsgo", "desktop-win", []string{"openid", "offline_access"}},
|
{"onlime", "Onlime (Denmark)", "cloud-auth.onlime.dk", "onlime_wl", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
|
{"mediamarkt", "MediaMarkt Cloud", "mediamarkt.jottacloud.com", "mediamarkt", "desktop", []string{"openid", "jotta-default", "offline_access"}},
|
||||||
|
{"letsgo", "Let's Go Cloud (Germany)", "letsgo.jotta.cloud", "letsgo", "desktop-win", []string{"openid", "offline_access"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,20 +178,32 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config
|
|||||||
}
|
}
|
||||||
return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Type of authentication.`, []fs.OptionExample{{
|
return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Type of authentication.`, []fs.OptionExample{{
|
||||||
Value: "standard",
|
Value: "standard",
|
||||||
Help: "Standard authentication.\nUse this if you're a normal Jottacloud user.",
|
Help: `Standard authentication.
|
||||||
|
This is primarily supported by the official service, but may also be supported
|
||||||
|
by some of the white-label services. It is designed for command-line
|
||||||
|
applications, and you will be asked to enter a single-use personal login token
|
||||||
|
which you must manually generate from the account security settings in the
|
||||||
|
web interface of your service.`,
|
||||||
}, {
|
}, {
|
||||||
Value: "whitelabel",
|
Value: "traditional",
|
||||||
Help: "Whitelabel authentication.\nUse this if you are using the service offered by a third party such as Telia, Tele2, Onlime, Elkjøp, etc.",
|
Help: `Traditional authentication.
|
||||||
|
This is supported by the official service and most of the white-label
|
||||||
|
services, you will be asked which service to connect to. You need to be on
|
||||||
|
a machine with an internet-connected web browser.`,
|
||||||
}, {
|
}, {
|
||||||
Value: "legacy",
|
Value: "legacy",
|
||||||
Help: "Legacy authentication.\nThis is no longer supported by any known services and not recommended for normal users.",
|
Help: `Legacy authentication.
|
||||||
|
This is no longer supported by any known services and not recommended used.
|
||||||
|
You will be asked for your account's username and password.`,
|
||||||
}})
|
}})
|
||||||
case "auth_type_done":
|
case "auth_type_done":
|
||||||
// Jump to next state according to config chosen
|
// Jump to next state according to config chosen
|
||||||
return fs.ConfigGoto(conf.Result)
|
return fs.ConfigGoto(conf.Result)
|
||||||
case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication
|
case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication
|
||||||
m.Set("configVersion", fmt.Sprint(configVersion))
|
m.Set("configVersion", fmt.Sprint(configVersion))
|
||||||
return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure")
|
return fs.ConfigInput("standard_token", "config_login_token", `Personal login token.
|
||||||
|
Generate it from the account security settings in the web interface of your
|
||||||
|
service, for the official service on https://www.jottacloud.com/web/secure.`)
|
||||||
case "standard_token":
|
case "standard_token":
|
||||||
loginToken := conf.Result
|
loginToken := conf.Result
|
||||||
m.Set(configClientID, defaultClientID)
|
m.Set(configClientID, defaultClientID)
|
||||||
@@ -206,29 +220,28 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config
|
|||||||
return nil, fmt.Errorf("error while saving token: %w", err)
|
return nil, fmt.Errorf("error while saving token: %w", err)
|
||||||
}
|
}
|
||||||
return fs.ConfigGoto("choose_device")
|
return fs.ConfigGoto("choose_device")
|
||||||
case "whitelabel":
|
case "traditional":
|
||||||
whitelabels := getWhitelabelServices()
|
services := getServices()
|
||||||
options := make([]fs.OptionExample, 0, len(whitelabels))
|
options := make([]fs.OptionExample, 0, len(services))
|
||||||
for key, val := range whitelabels {
|
for _, service := range services {
|
||||||
options = append(options, fs.OptionExample{
|
options = append(options, fs.OptionExample{
|
||||||
Value: key,
|
Value: service.key,
|
||||||
Help: val.name,
|
Help: service.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(options, func(i, j int) bool {
|
return fs.ConfigChooseExclusiveFixed("traditional_type", "config_traditional",
|
||||||
return options[i].Help < options[j].Help
|
|
||||||
})
|
|
||||||
return fs.ConfigChooseExclusiveFixed("whitelabel_type", "config_whitelabel",
|
|
||||||
"White-label service. This decides the domain name to connect to and\nthe authentication configuration to use.",
|
"White-label service. This decides the domain name to connect to and\nthe authentication configuration to use.",
|
||||||
options)
|
options)
|
||||||
case "whitelabel_type":
|
case "traditional_type":
|
||||||
whitelabel, ok := getWhitelabelServices()[conf.Result]
|
services := getServices()
|
||||||
if !ok {
|
i := slices.IndexFunc(services, func(s service) bool { return s.key == conf.Result })
|
||||||
return nil, fmt.Errorf("unexpected whitelabel %q", conf.Result)
|
if i == -1 {
|
||||||
|
return nil, fmt.Errorf("unexpected service %q", conf.Result)
|
||||||
}
|
}
|
||||||
|
service := services[i]
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
RootURL: "https://" + whitelabel.domain + "/auth/realms/" + whitelabel.realm + "/.well-known/openid-configuration",
|
RootURL: "https://" + service.domain + "/auth/realms/" + service.realm + "/.well-known/openid-configuration",
|
||||||
}
|
}
|
||||||
var wellKnown api.WellKnown
|
var wellKnown api.WellKnown
|
||||||
srv := rest.NewClient(fshttp.NewClient(ctx))
|
srv := rest.NewClient(fshttp.NewClient(ctx))
|
||||||
@@ -237,14 +250,14 @@ func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.Config
|
|||||||
return nil, fmt.Errorf("failed to get authentication provider configuration: %w", err)
|
return nil, fmt.Errorf("failed to get authentication provider configuration: %w", err)
|
||||||
}
|
}
|
||||||
m.Set("configVersion", fmt.Sprint(configVersion))
|
m.Set("configVersion", fmt.Sprint(configVersion))
|
||||||
m.Set(configClientID, whitelabel.clientID)
|
m.Set(configClientID, service.clientID)
|
||||||
m.Set(configTokenURL, wellKnown.TokenEndpoint)
|
m.Set(configTokenURL, wellKnown.TokenEndpoint)
|
||||||
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
|
return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
|
||||||
OAuth2Config: &oauthutil.Config{
|
OAuth2Config: &oauthutil.Config{
|
||||||
AuthURL: wellKnown.AuthorizationEndpoint,
|
AuthURL: wellKnown.AuthorizationEndpoint,
|
||||||
TokenURL: wellKnown.TokenEndpoint,
|
TokenURL: wellKnown.TokenEndpoint,
|
||||||
ClientID: whitelabel.clientID,
|
ClientID: service.clientID,
|
||||||
Scopes: whitelabel.scopes,
|
Scopes: service.scopes,
|
||||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
RedirectURL: oauthutil.RedirectLocalhostURL,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,60 +28,86 @@ as described [below](#whitelabel-authentication):
|
|||||||
- Onlime
|
- Onlime
|
||||||
- Onlime (onlime.dk)
|
- Onlime (onlime.dk)
|
||||||
- MediaMarkt
|
- MediaMarkt
|
||||||
|
- MediaMarkt Cloud (mediamarkt.jottacloud.com)
|
||||||
- Let's Go Cloud (letsgo.jotta.cloud)
|
- Let's Go Cloud (letsgo.jotta.cloud)
|
||||||
|
|
||||||
Paths are specified as `remote:path`
|
Paths are specified as `remote:path`
|
||||||
|
|
||||||
Paths may be as deep as required, e.g. `remote:directory/subdirectory`.
|
Paths may be as deep as required, e.g. `remote:directory/subdirectory`.
|
||||||
|
|
||||||
## Authentication types
|
## Authentication
|
||||||
|
|
||||||
Some of the white-label versions uses a different authentication method than the
|
Authentication in Jottacloud is in general based on OAuth 2.0 and OpenID
|
||||||
official service, and you have to choose the correct one when setting up the remote.
|
Connect (OIDC). There are different variants to choose from, described below.
|
||||||
|
Some of the variants are only supported by the official service and not
|
||||||
### Standard authentication
|
white-label services, so this must be taken into consideration when choosing.
|
||||||
|
|
||||||
The standard authentication method used by the official service (jottacloud.com),
|
|
||||||
as well as some of the white-label services, is based on OAuth 2.0 and OpenID
|
|
||||||
Connect (OIDC), and requires you to generate a single-use personal login token
|
|
||||||
from the account security settings in the service's web interface. Log in to your
|
|
||||||
account, go to "Settings" and then "Security", or use the direct link presented
|
|
||||||
to you by rclone when configuring the remote:
|
|
||||||
<https://www.jottacloud.com/web/secure>. Scroll down to the section "Personal login
|
|
||||||
token", and click the "Generate" button. Note that if you are using a white-label
|
|
||||||
service you probably can't use the direct link, you need to find the same page in
|
|
||||||
their dedicated web interface, and also it may be in a different location than
|
|
||||||
described above.
|
|
||||||
|
|
||||||
To access your account from multiple instances of rclone, you need to configure
|
To access your account from multiple instances of rclone, you need to configure
|
||||||
each of them with a separate personal login token. E.g. you create a Jottacloud
|
each of them separately. E.g. you create a Jottacloud remote with rclone in one
|
||||||
remote with rclone in one location, and copy the configuration file to a second
|
location, and copy the configuration file to a second location where you also
|
||||||
location where you also want to run rclone and access the same remote. Then you
|
want to run rclone and access the same remote. Then you need to replace the
|
||||||
need to replace the token for one of them, using the [config reconnect](https://rclone.org/commands/rclone_config_reconnect/)
|
token for one of them, using the [config reconnect](https://rclone.org/commands/rclone_config_reconnect/)
|
||||||
command, which requires you to generate a new personal login token and supply
|
command. For standard authentication (described below) this means you will have
|
||||||
as input. If you do not do this, the token may easily end up being invalidated,
|
to generate a new personal login token and supply as input. If you do not do
|
||||||
resulting in both instances failing with an error message something along the
|
this, the token may easily end up being invalidated, resulting in both
|
||||||
lines of:
|
instances failing with an error message something along the lines of:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
oauth2: cannot fetch token: 400 Bad Request
|
oauth2: cannot fetch token: 400 Bad Request
|
||||||
Response: {"error":"invalid_grant","error_description":"Stale token"}
|
Response: {"error":"invalid_grant","error_description":"Stale token"}
|
||||||
```
|
```
|
||||||
|
|
||||||
When this happens, you need to replace the token as described above to be able
|
The background for this is that OAuth tokens from Jottacloud normally have one
|
||||||
to use your remote again.
|
hour expiry, after which they will be automatically refreshed by rclone.
|
||||||
|
Jottacloud will then refresh not only the access token, but also the refresh
|
||||||
|
token. Any requests using a previous refresh token will be flagged, and lead
|
||||||
|
to the stale token error. When this happens, you need to replace the token as
|
||||||
|
described above to be able to use your remote again.
|
||||||
|
|
||||||
All personal login tokens you have taken into use will be listed in the web
|
Each time you are granted access with a new token, it will listed in the web
|
||||||
interface under "My logged in devices", and from the right side of that list
|
interface under "My logged in devices". From the right side of that list you
|
||||||
you can click the "X" button to revoke individual tokens.
|
can click the "X" button to revoke access. This will effectively disable the
|
||||||
|
refresh token, which means you will still have access using an existing access
|
||||||
|
token until that expires, but you will not be able to refresh it.
|
||||||
|
|
||||||
### Whitelabel authentication
|
### Standard
|
||||||
|
|
||||||
Most of the white-label versions uses a slightly different authentication flow,
|
This is an OAuth variant designed for command-line applications. It is
|
||||||
where it doesn't offer the option of creating a CLI token, and the username
|
primarily supported by the official service (jottacloud.com), but may also be
|
||||||
is generated internally. To setup rclone to use one of these, choose white-label
|
supported by some of the white-label services. The specific domain name and
|
||||||
authentication in the setup process, and then select the specific service
|
endpoint to connect to are found automatically (it is encoded into the supplied
|
||||||
in the next step.
|
login token, described next).
|
||||||
|
|
||||||
|
When configuring a remote, you are asked to enter a single-use personal login
|
||||||
|
token, which you must manually generate from the account security settings in
|
||||||
|
the service's web interface. You do not need a web browser on the same machine
|
||||||
|
like with traditional OAuth, but need to use a web browser somewhere, and be
|
||||||
|
able to be copy the generated string into your rclone configuration session.
|
||||||
|
|
||||||
|
Log in to your account, go to "Settings" and then "Security", or use the direct
|
||||||
|
link presented to you by rclone when configuring the remote:
|
||||||
|
<https://www.jottacloud.com/web/secure>. Scroll down to the section "Personal
|
||||||
|
login token", and click the "Generate" button. Note that if you are using a
|
||||||
|
white-label service you probably can't use the direct link, you need to find
|
||||||
|
the same page in their dedicated web interface, and also it may be in a
|
||||||
|
different location than described above.
|
||||||
|
|
||||||
|
When you have successfully authenticated using a personal login token, which
|
||||||
|
means you have received a proper OAuth token, there will be an entry in the
|
||||||
|
"My logged in devices" list in the web interface. It will be listed with
|
||||||
|
application name "Jottacloud CLI".
|
||||||
|
|
||||||
|
### Traditional
|
||||||
|
|
||||||
|
Jottacloud also supports a more traditional OAuth variant. Most of the
|
||||||
|
white-label services supports this, and often only this as they do not support
|
||||||
|
personal login tokens. This method relies on pre-defined domain names and
|
||||||
|
endpoints, and rclone must therefore explicitly add any white-label services
|
||||||
|
that should be supported.
|
||||||
|
|
||||||
|
When configuring a remote, you must interactively login to an OAuth
|
||||||
|
authorization web site, and a one-time authorization code are automatically
|
||||||
|
sent back to rclone, which it uses to request a token.
|
||||||
|
|
||||||
Note that when setting this up, you need to be on a machine with an
|
Note that when setting this up, you need to be on a machine with an
|
||||||
internet-connected web browser. If you need it on a machine where this is not
|
internet-connected web browser. If you need it on a machine where this is not
|
||||||
@@ -90,14 +116,18 @@ and copy it from there. The jottacloud backend does not support the
|
|||||||
`rclone authorize` command. See the [remote setup docs](/remote_setup) for
|
`rclone authorize` command. See the [remote setup docs](/remote_setup) for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
### Legacy authentication
|
When you have successfully authenticated, there will be an entry in the
|
||||||
|
"My logged in devices" list in the web interface. It will typically be listed
|
||||||
|
with application name "Jottacloud for Desktop" or similar (it depends on the
|
||||||
|
white-label service configuration).
|
||||||
|
|
||||||
Originally Jottacloud used an older authentication method, not based on OpenID
|
### Legacy
|
||||||
Connect, which required the username and password to be specified. Since
|
|
||||||
Jottacloud migrated to the newer method, handled by the standard authentication,
|
Originally Jottacloud used an OAuth variant which required your account's
|
||||||
some white-label versions (those from Elkjøp) still used the legacy method for
|
username and password to be specified. When Jottacloud migrated to the newer
|
||||||
a long time. Currently there are no known uses of this, it is still supported
|
methods, some white-label versions (those from Elkjøp) still used this legacy
|
||||||
by rclone, but the support will be removed in a future version.
|
method for a long time. Currently there are no known uses of this, it is still
|
||||||
|
supported by rclone, but the support will be removed in a future version.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -151,19 +181,27 @@ Type of authentication.
|
|||||||
Choose a number from below, or type in an existing value of type string.
|
Choose a number from below, or type in an existing value of type string.
|
||||||
Press Enter for the default (standard).
|
Press Enter for the default (standard).
|
||||||
/ Standard authentication.
|
/ Standard authentication.
|
||||||
1 | Use this if you're a normal Jottacloud user.
|
| This is primarily supported by the official service, but may also be supported
|
||||||
|
| by some of the white-label services. It is designed for command-line
|
||||||
|
1 | applications, and you will be asked to enter a single-use personal login token
|
||||||
|
| which you must manually generate from the account security settings in the
|
||||||
|
| web interface of your service.
|
||||||
\ (standard)
|
\ (standard)
|
||||||
/ Whitelabel authentication.
|
/ Traditional authentication.
|
||||||
2 | Use this if you are using the service offered by a third party such as Telia, Tele2, Onlime, Elkjøp, etc.
|
| This is supported by the official service and most of the white-label
|
||||||
\ (whitelabel)
|
2 | services, you will be asked which service to connect to. You need to be on
|
||||||
|
| a machine with an internet-connected web browser.
|
||||||
|
\ (traditional)
|
||||||
/ Legacy authentication.
|
/ Legacy authentication.
|
||||||
3 | This is no longer supported by any known services and not recommended for normal users.
|
3 | This is no longer supported by any known services and not recommended used.
|
||||||
|
| You will be asked for your account's username and password.
|
||||||
\ (legacy)
|
\ (legacy)
|
||||||
config_type> 1
|
config_type> 1
|
||||||
|
|
||||||
Option config_login_token.
|
Option config_login_token.
|
||||||
Personal login token.
|
Personal login token.
|
||||||
Generate here: https://www.jottacloud.com/web/secure
|
Generate it from the account security settings in the web interface of your
|
||||||
|
service, for the official service on https://www.jottacloud.com/web/secure.
|
||||||
Enter a value.
|
Enter a value.
|
||||||
config_login_token> <your token here>
|
config_login_token> <your token here>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user