Updated Authentication Daemon messages API (reponses).
parent
c8c52ea408
commit
fba25826de
|
@ -15,7 +15,6 @@ import Data.Tuple (Tuple(..))
|
||||||
import Data.ArrayBuffer.Types (ArrayBuffer)
|
import Data.ArrayBuffer.Types (ArrayBuffer)
|
||||||
|
|
||||||
import App.Email as Email
|
import App.Email as Email
|
||||||
import App.Phone as Phone
|
|
||||||
import App.UserPublic as UserPublic
|
import App.UserPublic as UserPublic
|
||||||
import App.PermissionLevel as PermissionLevel
|
import App.PermissionLevel as PermissionLevel
|
||||||
|
|
||||||
|
@ -27,32 +26,10 @@ import App.IPC as IPC
|
||||||
|
|
||||||
{- TODO:
|
{- TODO:
|
||||||
For a few messages, user can be designated by a string (login) or a number (its UID).
|
For a few messages, user can be designated by a string (login) or a number (its UID).
|
||||||
This was simplified by using the login for each.
|
This was simplified by using the uid for each.
|
||||||
Maybe this could be changed in the future to match the actual possibilities of the API.
|
Maybe this could be changed in the future to match the actual possibilities of the API.
|
||||||
|
|
||||||
Possible requests:
|
|
||||||
- 15 type EditProfileContent = { token :: Maybe String, shared_key :: Maybe String, user :: Int | String | Nil, new_profile :: Hash(String, JSON::Any) }
|
|
||||||
|
|
||||||
-- Deletion can be triggered by either an admin or the user.
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
-- Basic message types.
|
|
||||||
-- TODO: note to myself: messages seem chaotic. Could be simpler. Should be simpler.
|
|
||||||
type Error = { reason :: Maybe String }
|
|
||||||
type Token = { uid :: Int, token :: String }
|
|
||||||
type User = { user :: UserPublic.UserPublic }
|
|
||||||
type UserAdded = { user :: UserPublic.UserPublic }
|
|
||||||
type UserEdited = { uid :: Int }
|
|
||||||
type UserValidated = { user :: UserPublic.UserPublic }
|
|
||||||
type UsersList = { users :: Array UserPublic.UserPublic }
|
|
||||||
type PermissionCheck = { user :: Int, service :: String, resource :: String, permission :: PermissionLevel.PermissionLevel }
|
|
||||||
type PermissionSet = { user :: Int, service :: String, resource :: String, permission :: PermissionLevel.PermissionLevel }
|
|
||||||
type PasswordRecoverySent = { user :: UserPublic.UserPublic }
|
|
||||||
type PasswordRecovered = { user :: UserPublic.UserPublic }
|
|
||||||
type Contacts = { user :: Int, email :: Maybe Email.Email, phone :: Maybe Phone.Phone }
|
|
||||||
type MatchingUsers = { users :: Array UserPublic.UserPublic }
|
|
||||||
|
|
||||||
|
|
||||||
type Password = String
|
type Password = String
|
||||||
|
|
||||||
{- UserID should be in a separate module with a dedicated codec. -}
|
{- UserID should be in a separate module with a dedicated codec. -}
|
||||||
|
@ -185,42 +162,167 @@ codecSearchUser
|
||||||
{ regex: CAR.optional CA.string
|
{ regex: CAR.optional CA.string
|
||||||
, offset: CAR.optional CA.int })
|
, offset: CAR.optional CA.int })
|
||||||
|
|
||||||
-- Related JSON codecs.
|
|
||||||
|
{-
|
||||||
|
RESPONSES
|
||||||
|
-}
|
||||||
|
|
||||||
|
-- TODO: note to myself: messages seem chaotic. Could be simpler. Should be simpler.
|
||||||
|
{- 0 -}
|
||||||
|
type Error = { reason :: Maybe String }
|
||||||
codecGotError ∷ CA.JsonCodec Error
|
codecGotError ∷ CA.JsonCodec Error
|
||||||
codecGotError = CA.object "Error" (CAR.record { reason: CAR.optional CA.string })
|
codecGotError = CA.object "Error" (CAR.record { reason: CAR.optional CA.string })
|
||||||
codecGotToken ∷ CA.JsonCodec Token
|
|
||||||
codecGotToken = CA.object "Token" (CAR.record { "uid": CA.int, "token": CA.string })
|
{- 1 -}
|
||||||
codecGotPasswordRecoverySent ∷ CA.JsonCodec PasswordRecoverySent
|
type Logged = { uid :: Int, token :: String }
|
||||||
codecGotPasswordRecoverySent = CA.object "PasswordRecoverySent" (CAR.record { user: UserPublic.codec })
|
codecGotToken ∷ CA.JsonCodec Logged
|
||||||
|
codecGotToken = CA.object "Logged" (CAR.record { "uid": CA.int, "token": CA.string })
|
||||||
|
|
||||||
|
{- 2 -}
|
||||||
|
type User = { user :: UserPublic.UserPublic }
|
||||||
codecGotUser ∷ CA.JsonCodec User
|
codecGotUser ∷ CA.JsonCodec User
|
||||||
codecGotUser = CA.object "User" (CAR.record { user: UserPublic.codec })
|
codecGotUser = CA.object "User" (CAR.record { user: UserPublic.codec })
|
||||||
|
|
||||||
|
{- 3 -}
|
||||||
|
type UserAdded = { user :: UserPublic.UserPublic }
|
||||||
codecGotUserAdded ∷ CA.JsonCodec UserAdded
|
codecGotUserAdded ∷ CA.JsonCodec UserAdded
|
||||||
codecGotUserAdded = CA.object "UserAdded" (CAR.record { user: UserPublic.codec })
|
codecGotUserAdded = CA.object "UserAdded" (CAR.record { user: UserPublic.codec })
|
||||||
|
|
||||||
|
{- 4 -}
|
||||||
|
type UserEdited = { uid :: Int }
|
||||||
codecGotUserEdited ∷ CA.JsonCodec UserEdited
|
codecGotUserEdited ∷ CA.JsonCodec UserEdited
|
||||||
codecGotUserEdited = CA.object "UserEdited" (CAR.record { "uid": CA.int })
|
codecGotUserEdited = CA.object "UserEdited" (CAR.record { "uid": CA.int })
|
||||||
|
|
||||||
|
{- 5 -}
|
||||||
|
type UserValidated = { user :: UserPublic.UserPublic }
|
||||||
codecGotUserValidated ∷ CA.JsonCodec UserValidated
|
codecGotUserValidated ∷ CA.JsonCodec UserValidated
|
||||||
codecGotUserValidated = CA.object "UserValidated" (CAR.record { user: UserPublic.codec })
|
codecGotUserValidated = CA.object "UserValidated" (CAR.record { user: UserPublic.codec })
|
||||||
codecGotPasswordRecovered ∷ CA.JsonCodec PasswordRecovered
|
|
||||||
codecGotPasswordRecovered = CA.object "PasswordRecovered" (CAR.record { user: UserPublic.codec })
|
{- 6 -}
|
||||||
|
type UsersList = { users :: Array UserPublic.UserPublic }
|
||||||
codecGotUsersList ∷ CA.JsonCodec UsersList
|
codecGotUsersList ∷ CA.JsonCodec UsersList
|
||||||
codecGotUsersList = CA.object "UsersList" (CAR.record { users: CA.array UserPublic.codec })
|
codecGotUsersList = CA.object "UsersList" (CAR.record { users: CA.array UserPublic.codec })
|
||||||
|
|
||||||
|
{- 7 -}
|
||||||
|
type PermissionCheck
|
||||||
|
= { user :: Int
|
||||||
|
, service :: String
|
||||||
|
, resource :: String
|
||||||
|
, permission :: PermissionLevel.PermissionLevel }
|
||||||
codecGotPermissionCheck :: CA.JsonCodec PermissionCheck
|
codecGotPermissionCheck :: CA.JsonCodec PermissionCheck
|
||||||
codecGotPermissionCheck = CA.object "PermissionCheck" (CAR.record { user: CA.int
|
codecGotPermissionCheck
|
||||||
, service: CA.string
|
= CA.object "PermissionCheck" (CAR.record
|
||||||
, resource: CA.string
|
{ user: CA.int
|
||||||
, permission: PermissionLevel.codec })
|
, service: CA.string
|
||||||
|
, resource: CA.string
|
||||||
|
, permission: PermissionLevel.codec })
|
||||||
|
|
||||||
|
{- 8 -}
|
||||||
|
type PermissionSet
|
||||||
|
= { user :: Int
|
||||||
|
, service :: String
|
||||||
|
, resource :: String
|
||||||
|
, permission :: PermissionLevel.PermissionLevel }
|
||||||
codecGotPermissionSet :: CA.JsonCodec PermissionSet
|
codecGotPermissionSet :: CA.JsonCodec PermissionSet
|
||||||
codecGotPermissionSet = CA.object "PermissionSet" (CAR.record { user: CA.int
|
codecGotPermissionSet
|
||||||
, service: CA.string
|
= CA.object "PermissionSet" (CAR.record
|
||||||
, resource: CA.string
|
{ user: CA.int
|
||||||
, permission: PermissionLevel.codec })
|
, service: CA.string
|
||||||
codecGotContacts ∷ CA.JsonCodec Contacts
|
, resource: CA.string
|
||||||
codecGotContacts = CA.object "Contacts" (CAR.record { user: CA.int
|
, permission: PermissionLevel.codec })
|
||||||
, email: CAR.optional Email.codec
|
|
||||||
, phone: CAR.optional Phone.codec })
|
{- 9 -}
|
||||||
|
type PasswordRecoverySent = { user :: UserPublic.UserPublic }
|
||||||
|
codecGotPasswordRecoverySent ∷ CA.JsonCodec PasswordRecoverySent
|
||||||
|
codecGotPasswordRecoverySent
|
||||||
|
= CA.object "PasswordRecoverySent" (CAR.record { user: UserPublic.codec })
|
||||||
|
|
||||||
|
{- 10 -}
|
||||||
|
type PasswordRecovered = { }
|
||||||
|
codecGotPasswordRecovered ∷ CA.JsonCodec PasswordRecovered
|
||||||
|
codecGotPasswordRecovered = CA.object "PasswordRecovered" (CAR.record { })
|
||||||
|
|
||||||
|
{- 11 -}
|
||||||
|
type MatchingUsers = { users :: Array UserPublic.UserPublic }
|
||||||
codecGotMatchingUsers ∷ CA.JsonCodec MatchingUsers
|
codecGotMatchingUsers ∷ CA.JsonCodec MatchingUsers
|
||||||
codecGotMatchingUsers = CA.object "MatchingUsers" (CAR.record { users: CA.array UserPublic.codec })
|
codecGotMatchingUsers = CA.object "MatchingUsers" (CAR.record { users: CA.array UserPublic.codec })
|
||||||
|
|
||||||
|
{- 20 -}
|
||||||
|
type ErrorMustBeAuthenticated = {}
|
||||||
|
codecGotErrorMustBeAuthenticated :: CA.JsonCodec ErrorMustBeAuthenticated
|
||||||
|
codecGotErrorMustBeAuthenticated = CA.object "ErrorMustBeAuthenticated" (CAR.record {})
|
||||||
|
|
||||||
|
{- 21 -}
|
||||||
|
type ErrorAlreadyUsedLogin = {}
|
||||||
|
codecGotErrorAlreadyUsedLogin :: CA.JsonCodec ErrorAlreadyUsedLogin
|
||||||
|
codecGotErrorAlreadyUsedLogin = CA.object "ErrorAlreadyUsedLogin" (CAR.record {})
|
||||||
|
|
||||||
|
{- 22 -}
|
||||||
|
type ErrorMailRequired = {}
|
||||||
|
codecGotErrorMailRequired :: CA.JsonCodec ErrorMailRequired
|
||||||
|
codecGotErrorMailRequired = CA.object "ErrorMailRequired" (CAR.record {})
|
||||||
|
|
||||||
|
{- 23 -}
|
||||||
|
type ErrorUserNotFound = {}
|
||||||
|
codecGotErrorUserNotFound :: CA.JsonCodec ErrorUserNotFound
|
||||||
|
codecGotErrorUserNotFound = CA.object "ErrorUserNotFound" (CAR.record {})
|
||||||
|
|
||||||
|
{- 24 -}
|
||||||
|
type ErrorPasswordTooShort = {}
|
||||||
|
codecGotErrorPasswordTooShort :: CA.JsonCodec ErrorPasswordTooShort
|
||||||
|
codecGotErrorPasswordTooShort = CA.object "ErrorPasswordTooShort" (CAR.record {})
|
||||||
|
|
||||||
|
{- 25 -}
|
||||||
|
type ErrorInvalidCredentials = {}
|
||||||
|
codecGotErrorInvalidCredentials :: CA.JsonCodec ErrorInvalidCredentials
|
||||||
|
codecGotErrorInvalidCredentials = CA.object "ErrorInvalidCredentials" (CAR.record {})
|
||||||
|
|
||||||
|
{- 26 -}
|
||||||
|
type ErrorRegistrationsClosed = {}
|
||||||
|
codecGotErrorRegistrationsClosed :: CA.JsonCodec ErrorRegistrationsClosed
|
||||||
|
codecGotErrorRegistrationsClosed = CA.object "ErrorRegistrationsClosed" (CAR.record {})
|
||||||
|
|
||||||
|
{- 27 -}
|
||||||
|
type ErrorInvalidLoginFormat = {}
|
||||||
|
codecGotErrorInvalidLoginFormat :: CA.JsonCodec ErrorInvalidLoginFormat
|
||||||
|
codecGotErrorInvalidLoginFormat = CA.object "ErrorInvalidLoginFormat" (CAR.record {})
|
||||||
|
|
||||||
|
{- 28 -}
|
||||||
|
type ErrorInvalidEmailFormat = {}
|
||||||
|
codecGotErrorInvalidEmailFormat :: CA.JsonCodec ErrorInvalidEmailFormat
|
||||||
|
codecGotErrorInvalidEmailFormat = CA.object "ErrorInvalidEmailFormat" (CAR.record {})
|
||||||
|
|
||||||
|
{- 29 -}
|
||||||
|
type ErrorAlreadyUsersInDB = {}
|
||||||
|
codecGotErrorAlreadyUsersInDB :: CA.JsonCodec ErrorAlreadyUsersInDB
|
||||||
|
codecGotErrorAlreadyUsersInDB = CA.object "ErrorAlreadyUsersInDB" (CAR.record {})
|
||||||
|
|
||||||
|
{- 30 -}
|
||||||
|
type ErrorReadOnlyProfileKeys = { read_only_keys :: Array String }
|
||||||
|
codecGotErrorReadOnlyProfileKeys :: CA.JsonCodec ErrorReadOnlyProfileKeys
|
||||||
|
codecGotErrorReadOnlyProfileKeys
|
||||||
|
= CA.object "ErrorReadOnlyProfileKeys" (CAR.record { read_only_keys: CA.array CA.string })
|
||||||
|
|
||||||
|
{- 31 -}
|
||||||
|
type ErrorInvalidActivationKey = {}
|
||||||
|
codecGotErrorInvalidActivationKey :: CA.JsonCodec ErrorInvalidActivationKey
|
||||||
|
codecGotErrorInvalidActivationKey = CA.object "ErrorInvalidActivationKey" (CAR.record {})
|
||||||
|
|
||||||
|
{- 32 -}
|
||||||
|
type ErrorUserAlreadyValidated = {}
|
||||||
|
codecGotErrorUserAlreadyValidated :: CA.JsonCodec ErrorUserAlreadyValidated
|
||||||
|
codecGotErrorUserAlreadyValidated = CA.object "ErrorUserAlreadyValidated" (CAR.record {})
|
||||||
|
|
||||||
|
{- 33 -}
|
||||||
|
type ErrorCannotContactUser = {}
|
||||||
|
codecGotErrorCannotContactUser :: CA.JsonCodec ErrorCannotContactUser
|
||||||
|
codecGotErrorCannotContactUser = CA.object "ErrorCannotContactUser" (CAR.record {})
|
||||||
|
|
||||||
|
{- 34 -}
|
||||||
|
type ErrorInvalidRenewKey = {}
|
||||||
|
codecGotErrorInvalidRenewKey :: CA.JsonCodec ErrorInvalidRenewKey
|
||||||
|
codecGotErrorInvalidRenewKey = CA.object "ErrorInvalidRenewKey" (CAR.record {})
|
||||||
|
|
||||||
-- All possible requests.
|
-- All possible requests.
|
||||||
data RequestMessage
|
data RequestMessage
|
||||||
= MkLogin Login -- 0
|
= MkLogin Login -- 0
|
||||||
|
@ -240,19 +342,33 @@ data RequestMessage
|
||||||
|
|
||||||
-- All possible answers from the authentication daemon (authd).
|
-- All possible answers from the authentication daemon (authd).
|
||||||
data AnswerMessage
|
data AnswerMessage
|
||||||
= GotError Error -- 0
|
= GotError Error -- 0
|
||||||
| GotToken Token -- 1
|
| GotToken Logged -- 1
|
||||||
| GotUser User -- 2
|
| GotUser User -- 2
|
||||||
| GotUserAdded UserAdded -- 3
|
| GotUserAdded UserAdded -- 3
|
||||||
| GotUserEdited UserEdited -- 4
|
| GotUserEdited UserEdited -- 4
|
||||||
| GotUserValidated UserValidated -- 5
|
| GotUserValidated UserValidated -- 5
|
||||||
| GotUsersList UsersList -- 6
|
| GotUsersList UsersList -- 6
|
||||||
| GotPermissionCheck PermissionCheck -- 7
|
| GotPermissionCheck PermissionCheck -- 7
|
||||||
| GotPermissionSet PermissionSet -- 8
|
| GotPermissionSet PermissionSet -- 8
|
||||||
| GotPasswordRecoverySent PasswordRecoverySent -- 9
|
| GotPasswordRecoverySent PasswordRecoverySent -- 9
|
||||||
| GotPasswordRecovered PasswordRecovered -- 10
|
| GotPasswordRecovered PasswordRecovered -- 10
|
||||||
| GotMatchingUsers MatchingUsers -- 11
|
| GotMatchingUsers MatchingUsers -- 11
|
||||||
| GotContacts Contacts -- 12
|
| GotErrorMustBeAuthenticated ErrorMustBeAuthenticated -- 20
|
||||||
|
| GotErrorAlreadyUsedLogin ErrorAlreadyUsedLogin -- 21
|
||||||
|
| GotErrorMailRequired ErrorMailRequired -- 22
|
||||||
|
| GotErrorUserNotFound ErrorUserNotFound -- 23
|
||||||
|
| GotErrorPasswordTooShort ErrorPasswordTooShort -- 24
|
||||||
|
| GotErrorInvalidCredentials ErrorInvalidCredentials -- 25
|
||||||
|
| GotErrorRegistrationsClosed ErrorRegistrationsClosed -- 26
|
||||||
|
| GotErrorInvalidLoginFormat ErrorInvalidLoginFormat -- 27
|
||||||
|
| GotErrorInvalidEmailFormat ErrorInvalidEmailFormat -- 28
|
||||||
|
| GotErrorAlreadyUsersInDB ErrorAlreadyUsersInDB -- 29
|
||||||
|
| GotErrorReadOnlyProfileKeys ErrorReadOnlyProfileKeys -- 30
|
||||||
|
| GotErrorInvalidActivationKey ErrorInvalidActivationKey -- 31
|
||||||
|
| GotErrorUserAlreadyValidated ErrorUserAlreadyValidated -- 32
|
||||||
|
| GotErrorCannotContactUser ErrorCannotContactUser -- 33
|
||||||
|
| GotErrorInvalidRenewKey ErrorInvalidRenewKey -- 34
|
||||||
|
|
||||||
encode ∷ RequestMessage -> Tuple UInt String
|
encode ∷ RequestMessage -> Tuple UInt String
|
||||||
encode m = case m of
|
encode m = case m of
|
||||||
|
@ -284,19 +400,33 @@ data DecodeError
|
||||||
decode :: Int -> String -> Either DecodeError AnswerMessage
|
decode :: Int -> String -> Either DecodeError AnswerMessage
|
||||||
decode number string
|
decode number string
|
||||||
= case number of
|
= case number of
|
||||||
0 -> error_management codecGotError GotError
|
0 -> error_management codecGotError GotError
|
||||||
1 -> error_management codecGotToken GotToken
|
1 -> error_management codecGotToken GotToken
|
||||||
2 -> error_management codecGotUser GotUser
|
2 -> error_management codecGotUser GotUser
|
||||||
3 -> error_management codecGotUserAdded GotUserAdded
|
3 -> error_management codecGotUserAdded GotUserAdded
|
||||||
4 -> error_management codecGotUserEdited GotUserEdited
|
4 -> error_management codecGotUserEdited GotUserEdited
|
||||||
5 -> error_management codecGotUserValidated GotUserValidated
|
5 -> error_management codecGotUserValidated GotUserValidated
|
||||||
6 -> error_management codecGotUsersList GotUsersList
|
6 -> error_management codecGotUsersList GotUsersList
|
||||||
7 -> error_management codecGotPermissionCheck GotPermissionCheck
|
7 -> error_management codecGotPermissionCheck GotPermissionCheck
|
||||||
8 -> error_management codecGotPermissionSet GotPermissionSet
|
8 -> error_management codecGotPermissionSet GotPermissionSet
|
||||||
9 -> error_management codecGotPasswordRecoverySent GotPasswordRecoverySent
|
9 -> error_management codecGotPasswordRecoverySent GotPasswordRecoverySent
|
||||||
10 -> error_management codecGotPasswordRecovered GotPasswordRecovered
|
10 -> error_management codecGotPasswordRecovered GotPasswordRecovered
|
||||||
11 -> error_management codecGotMatchingUsers GotMatchingUsers
|
11 -> error_management codecGotMatchingUsers GotMatchingUsers
|
||||||
12 -> error_management codecGotContacts GotContacts
|
20 -> error_management codecGotErrorMustBeAuthenticated GotErrorMustBeAuthenticated
|
||||||
|
21 -> error_management codecGotErrorAlreadyUsedLogin GotErrorAlreadyUsedLogin
|
||||||
|
22 -> error_management codecGotErrorMailRequired GotErrorMailRequired
|
||||||
|
23 -> error_management codecGotErrorUserNotFound GotErrorUserNotFound
|
||||||
|
24 -> error_management codecGotErrorPasswordTooShort GotErrorPasswordTooShort
|
||||||
|
25 -> error_management codecGotErrorInvalidCredentials GotErrorInvalidCredentials
|
||||||
|
26 -> error_management codecGotErrorRegistrationsClosed GotErrorRegistrationsClosed
|
||||||
|
27 -> error_management codecGotErrorInvalidLoginFormat GotErrorInvalidLoginFormat
|
||||||
|
28 -> error_management codecGotErrorInvalidEmailFormat GotErrorInvalidEmailFormat
|
||||||
|
29 -> error_management codecGotErrorAlreadyUsersInDB GotErrorAlreadyUsersInDB
|
||||||
|
30 -> error_management codecGotErrorReadOnlyProfileKeys GotErrorReadOnlyProfileKeys
|
||||||
|
31 -> error_management codecGotErrorInvalidActivationKey GotErrorInvalidActivationKey
|
||||||
|
32 -> error_management codecGotErrorUserAlreadyValidated GotErrorUserAlreadyValidated
|
||||||
|
33 -> error_management codecGotErrorCannotContactUser GotErrorCannotContactUser
|
||||||
|
34 -> error_management codecGotErrorInvalidRenewKey GotErrorInvalidRenewKey
|
||||||
_ -> Left UnknownNumber
|
_ -> Left UnknownNumber
|
||||||
where
|
where
|
||||||
-- Signature is required since the compiler's guess is wrong.
|
-- Signature is required since the compiler's guess is wrong.
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
-- | TODO: Phone module should include at least some sort of smart
|
|
||||||
-- | constructors, rejecting invalid phone numbers.
|
|
||||||
module App.Phone where
|
|
||||||
|
|
||||||
import Prelude
|
|
||||||
|
|
||||||
import Data.Codec.Argonaut (JsonCodec)
|
|
||||||
import Data.Codec.Argonaut as CA
|
|
||||||
import Data.Newtype (class Newtype)
|
|
||||||
import Data.Profunctor (wrapIso)
|
|
||||||
|
|
||||||
newtype Phone = Phone String
|
|
||||||
|
|
||||||
derive instance newtypePhone :: Newtype Phone _
|
|
||||||
derive instance eqPhone :: Eq Phone
|
|
||||||
derive instance ordPhone :: Ord Phone
|
|
||||||
|
|
||||||
-- | Phone.codec can be used to parse and encode phone numbers.
|
|
||||||
codec :: JsonCodec Phone
|
|
||||||
codec = wrapIso Phone CA.string
|
|
Loading…
Reference in New Issue