cone.firebase 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cone/firebase/__init__.py +87 -0
- cone/firebase/api.py +5 -0
- cone/firebase/authentication.py +76 -0
- cone/firebase/management.py +181 -0
- cone/firebase/messaging.py +52 -0
- cone/firebase/testing/__init__.py +133 -0
- cone/firebase/testing/firebase_admin/__init__.py +54 -0
- cone/firebase/testing/firebase_admin/credentials.py +4 -0
- cone/firebase/testing/firebase_admin/messaging.py +121 -0
- cone/firebase/tests.py +112 -0
- cone.firebase-0.1-py3.9-nspkg.pth +1 -0
- cone_firebase-0.1.dist-info/METADATA +146 -0
- cone_firebase-0.1.dist-info/RECORD +17 -0
- cone_firebase-0.1.dist-info/WHEEL +5 -0
- cone_firebase-0.1.dist-info/licenses/LICENSE.rst +27 -0
- cone_firebase-0.1.dist-info/namespace_packages.txt +1 -0
- cone_firebase-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from cone.app import main_hook
|
|
2
|
+
from cone.app.interfaces import IAuthenticator
|
|
3
|
+
from cone.firebase.authentication import FirebaseAuthenticator
|
|
4
|
+
from cone.ugm.browser.principal import user_field
|
|
5
|
+
from yafowil.base import factory
|
|
6
|
+
import firebase_admin
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger('cone.firebase')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FirebaseConfig(object):
|
|
15
|
+
web_api_key = None
|
|
16
|
+
service_account_json = None
|
|
17
|
+
|
|
18
|
+
def __init__(self, web_api_key, service_account_json_file):
|
|
19
|
+
self.web_api_key = web_api_key
|
|
20
|
+
with open(service_account_json_file, 'r') as f:
|
|
21
|
+
self.service_account_json = json.loads(f.read())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# FirebaseConfig singleton
|
|
25
|
+
config = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_firebase_config(settings):
|
|
29
|
+
global config
|
|
30
|
+
config = FirebaseConfig(
|
|
31
|
+
settings['firebase.web_api_key'],
|
|
32
|
+
settings['firebase.service_account_json_file']
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def initialize_firebase_admin():
|
|
37
|
+
service_account_json = config.service_account_json
|
|
38
|
+
cred = firebase_admin.credentials.Certificate(service_account_json)
|
|
39
|
+
if not firebase_admin._apps:
|
|
40
|
+
firebase_admin.initialize_app(cred)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@user_field('firebase_user')
|
|
44
|
+
def firebase_user_field_factory(form, label, value):
|
|
45
|
+
return factory(
|
|
46
|
+
'field:label:help:error:checkbox',
|
|
47
|
+
value=value,
|
|
48
|
+
props={
|
|
49
|
+
'label': label,
|
|
50
|
+
'datatype': bool,
|
|
51
|
+
'help': "shall this user also be managed in Firebase?",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@user_field('email_verified')
|
|
56
|
+
def email_verified(form, label, value):
|
|
57
|
+
return factory(
|
|
58
|
+
'field:label:help:error:checkbox',
|
|
59
|
+
value=value,
|
|
60
|
+
props={
|
|
61
|
+
'label': label,
|
|
62
|
+
'datatype': bool,
|
|
63
|
+
'help': "is user verified by email?",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@user_field('fullname')
|
|
68
|
+
def fullname_field_factory(form, label, value):
|
|
69
|
+
return factory(
|
|
70
|
+
'field:label:error:text',
|
|
71
|
+
value=value,
|
|
72
|
+
props={
|
|
73
|
+
'label': label,
|
|
74
|
+
'datatype': str,
|
|
75
|
+
'required': "fullname not given"
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@main_hook
|
|
80
|
+
def initialize_firebase(config, global_config, settings):
|
|
81
|
+
load_firebase_config(settings)
|
|
82
|
+
initialize_firebase_admin()
|
|
83
|
+
config.registry.registerUtility(
|
|
84
|
+
FirebaseAuthenticator,
|
|
85
|
+
IAuthenticator,
|
|
86
|
+
'firebase'
|
|
87
|
+
)
|
cone/firebase/api.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from cone.firebase.management import get_device_tokens_for_user # noqa
|
|
2
|
+
from cone.firebase.management import register_device_token_for_user # noqa
|
|
3
|
+
from cone.firebase.messaging import send_message # noqa
|
|
4
|
+
from cone.firebase.messaging import send_message_to_user # noqa
|
|
5
|
+
from cone.firebase.messaging import send_messages # noqa
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from cone.app import security
|
|
2
|
+
from cone.app import ugm_backend
|
|
3
|
+
from cone.app.interfaces import IAuthenticator
|
|
4
|
+
from zope.interface import implementer
|
|
5
|
+
import cone.firebase
|
|
6
|
+
import json
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
REST_API_URL_LOGIN = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sign_in_with_email_and_password(email, password, api_key, return_secure_token = True):
|
|
14
|
+
"""
|
|
15
|
+
borrowed from here:
|
|
16
|
+
https://github.com/billydh/python-firebase-admin-sdk-demo/blob/master/sign_in_with_email_and_password.py
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
payload = json.dumps({
|
|
21
|
+
"email": email or "",
|
|
22
|
+
"password": password or "",
|
|
23
|
+
"returnSecureToken": return_secure_token
|
|
24
|
+
})
|
|
25
|
+
except Exception as ex:
|
|
26
|
+
cone.firebase.logger.error(f"error encoding email: {email} and password: {password}")
|
|
27
|
+
raise
|
|
28
|
+
|
|
29
|
+
r = requests.post(REST_API_URL_LOGIN,
|
|
30
|
+
params={"key": api_key},
|
|
31
|
+
data=payload)
|
|
32
|
+
|
|
33
|
+
return r.json()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@implementer(IAuthenticator)
|
|
37
|
+
class FirebaseAuthenticator:
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def authenticate(cls, uid: str, pwd: str) -> str:
|
|
41
|
+
"""
|
|
42
|
+
does firebase authentication and in the case of success
|
|
43
|
+
checks if the uid exists in self.users, if not it creates the user there
|
|
44
|
+
when fb login is unsuccessful delegates it to
|
|
45
|
+
:return: login of logged in user
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
res = sign_in_with_email_and_password(uid, pwd, cone.firebase.config.web_api_key)
|
|
49
|
+
|
|
50
|
+
ugm = ugm_backend.ugm
|
|
51
|
+
users = ugm.users
|
|
52
|
+
|
|
53
|
+
if res.get("kind") == 'identitytoolkit#VerifyPasswordResponse':
|
|
54
|
+
id = res["localId"]
|
|
55
|
+
if id not in users:
|
|
56
|
+
users.create(
|
|
57
|
+
id,
|
|
58
|
+
login="email",
|
|
59
|
+
email=res["email"],
|
|
60
|
+
fullname=res.get("displayName"),
|
|
61
|
+
registered=res["registered"],
|
|
62
|
+
idtoken=res["idToken"]
|
|
63
|
+
)
|
|
64
|
+
# else:
|
|
65
|
+
# user email aktualisieren
|
|
66
|
+
|
|
67
|
+
user = users[id]
|
|
68
|
+
if hasattr(users, "on_authenticated"):
|
|
69
|
+
users.on_authenticated(res["localId"])
|
|
70
|
+
# user.passwd(None, pwd) # it is to decide if we need local pwds
|
|
71
|
+
return id
|
|
72
|
+
else:
|
|
73
|
+
# if not found in firebase login locally
|
|
74
|
+
auth = users.authenticate(uid, pwd)
|
|
75
|
+
if auth:
|
|
76
|
+
return uid
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from cone.app import ugm_backend
|
|
2
|
+
from cone.ugm.events import UserCreatedEvent
|
|
3
|
+
from cone.ugm.events import UserDeletedEvent
|
|
4
|
+
from cone.ugm.events import UserModifiedEvent
|
|
5
|
+
from firebase_admin import auth
|
|
6
|
+
from firebase_admin.auth import UserNotFoundError
|
|
7
|
+
from pyramid.security import remember
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
from yafowil.base import ExtractionError
|
|
11
|
+
from zope.event import classhandler
|
|
12
|
+
import cone.firebase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
FIREBASE_DEVICE_TOKENS = "firebase_device_tokens"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@classhandler.handler(UserCreatedEvent)
|
|
19
|
+
def on_user_created(event: UserCreatedEvent):
|
|
20
|
+
user = event.principal
|
|
21
|
+
email = user.attrs["mail"]
|
|
22
|
+
uid = user.attrs["id"]
|
|
23
|
+
# if firebase_user checkbox is not installed, we want to add all users to FB
|
|
24
|
+
if "firebase_user" not in user.attrs or user.attrs["firebase_user"]:
|
|
25
|
+
if email:
|
|
26
|
+
user_record = create_firebase_user(user, event.password)
|
|
27
|
+
cone.firebase.logger.info(f"user {uid} added to firebase with email {email} -> {user_record.__dict__}")
|
|
28
|
+
else:
|
|
29
|
+
cone.firebase.logger.warn(f"user {uid} has no email -> not added to firebase")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_firebase_user(user, password):
|
|
33
|
+
fullname = user.attrs["fullname"]
|
|
34
|
+
user_record = auth.create_user(
|
|
35
|
+
uid=user.attrs["id"],
|
|
36
|
+
email=user.attrs["mail"],
|
|
37
|
+
# phone_number='+15555550100',
|
|
38
|
+
# email_verified=True,
|
|
39
|
+
password=password,
|
|
40
|
+
display_name=fullname,
|
|
41
|
+
# photo_url='http://www.example.com/12345678/photo.png',
|
|
42
|
+
disabled=False
|
|
43
|
+
)
|
|
44
|
+
user.attrs["login"] = "mail"
|
|
45
|
+
return user_record
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@classhandler.handler(UserModifiedEvent)
|
|
49
|
+
def on_user_modified(event: UserModifiedEvent):
|
|
50
|
+
user = event.principal
|
|
51
|
+
email = user.attrs["mail"]
|
|
52
|
+
uid = user.attrs["id"]
|
|
53
|
+
try:
|
|
54
|
+
fbuser = auth.get_user(uid)
|
|
55
|
+
except UserNotFoundError:
|
|
56
|
+
# user does not exist in firebase, lets push it to fb, the password has to be specified by hand, otherwise the
|
|
57
|
+
# hashed password will be set in fb!
|
|
58
|
+
if user.attrs.get("firebase_user", False):
|
|
59
|
+
cone.firebase.logger.warn(f"user wth id {uid} not found in firebase, creating it in fb")
|
|
60
|
+
if not user.attrs["fullname"]:
|
|
61
|
+
raise ExtractionError("Fullname not given")
|
|
62
|
+
fbuser = create_firebase_user(user, event.password)
|
|
63
|
+
cone.firebase.logger.warn(f"created user wth id {uid} in fb")
|
|
64
|
+
else:
|
|
65
|
+
fbuser = None
|
|
66
|
+
cone.firebase.logger.warn(f"user wth id {uid} not found in firebase")
|
|
67
|
+
|
|
68
|
+
if email and fbuser:
|
|
69
|
+
fullname = user.attrs["fullname"]
|
|
70
|
+
password = event.password
|
|
71
|
+
|
|
72
|
+
params = dict(
|
|
73
|
+
email=email,
|
|
74
|
+
# phone_number='+15555550100',
|
|
75
|
+
display_name=fullname,
|
|
76
|
+
disabled=False)
|
|
77
|
+
|
|
78
|
+
if event.password:
|
|
79
|
+
params["password"] = event.password
|
|
80
|
+
|
|
81
|
+
res = auth.update_user(
|
|
82
|
+
uid,
|
|
83
|
+
**params
|
|
84
|
+
)
|
|
85
|
+
cone.firebase.logger.info(f"user {uid} changes promoted to firebase with email {email} -> {res}")
|
|
86
|
+
else:
|
|
87
|
+
cone.firebase.logger.warn(f"user {uid} has no email -> not added to firebase")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@classhandler.handler(UserDeletedEvent)
|
|
91
|
+
def on_user_deleted(event):
|
|
92
|
+
user = event.principal
|
|
93
|
+
uid = user.attrs["id"]
|
|
94
|
+
try:
|
|
95
|
+
fbuser = auth.get_user(uid)
|
|
96
|
+
auth.delete_user(uid)
|
|
97
|
+
cone.firebase.logger.info(f"user with id {uid} deleted in firebase")
|
|
98
|
+
except UserNotFoundError:
|
|
99
|
+
cone.firebase.logger.warn(f"user with id {uid} not found in firebase -> user not deleted in fb")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def authenticate_with_id_token(request, id_token: str) -> Tuple[str, str]:
|
|
103
|
+
"""
|
|
104
|
+
uses the firebase ID token to login without password
|
|
105
|
+
needs installed UGM user folder located at AppRoot()["users"]
|
|
106
|
+
|
|
107
|
+
:return: tuple (user_id, auth token)
|
|
108
|
+
"""
|
|
109
|
+
# users = AppRoot()["users"]
|
|
110
|
+
cone.firebase.logger.info(f"loggin in with firebase id token: {id_token}")
|
|
111
|
+
ugm = ugm_backend.ugm
|
|
112
|
+
users = ugm.users
|
|
113
|
+
res = auth.verify_id_token(id_token)
|
|
114
|
+
user_id = res["user_id"]
|
|
115
|
+
if user_id not in users:
|
|
116
|
+
# TODO: creation shall take place in security.authenticate()
|
|
117
|
+
cone.firebase.logger.info(f"user with id {user_id} not stored locally - creating")
|
|
118
|
+
users.create(
|
|
119
|
+
user_id,
|
|
120
|
+
login="email",
|
|
121
|
+
email=res["email"],
|
|
122
|
+
fullname=res.get("name", ""),
|
|
123
|
+
email_verified=res["email_verified"],
|
|
124
|
+
phone=res.get("phone_number", ""),
|
|
125
|
+
idtoken=id_token
|
|
126
|
+
)
|
|
127
|
+
cone.firebase.logger.info(f"user with {user_id} successfully created")
|
|
128
|
+
|
|
129
|
+
return user_id, remember(request, user_id)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def register_device_token_for_user(login: str, token: str) -> List[str]:
|
|
133
|
+
"""
|
|
134
|
+
registers a device token for a given user
|
|
135
|
+
:param login: email or uid
|
|
136
|
+
:param token: firebase device token
|
|
137
|
+
"""
|
|
138
|
+
users = ugm_backend.ugm.users
|
|
139
|
+
if login not in users:
|
|
140
|
+
uid = users.id_for_login(login)
|
|
141
|
+
else:
|
|
142
|
+
uid = login
|
|
143
|
+
|
|
144
|
+
user = users[uid]
|
|
145
|
+
tokens = user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
|
|
146
|
+
if token not in tokens:
|
|
147
|
+
user.attrs[FIREBASE_DEVICE_TOKENS] = list(tokens) + [token]
|
|
148
|
+
|
|
149
|
+
return user.attrs[FIREBASE_DEVICE_TOKENS]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def unregister_device_token_for_user(login: str, token: str):
|
|
153
|
+
"""
|
|
154
|
+
registers a device token for a given user
|
|
155
|
+
:param login: email or uid
|
|
156
|
+
:param token: firebase device token
|
|
157
|
+
"""
|
|
158
|
+
users = ugm_backend.ugm.users
|
|
159
|
+
if login not in users:
|
|
160
|
+
uid = users.id_for_login(login)
|
|
161
|
+
else:
|
|
162
|
+
uid = login
|
|
163
|
+
|
|
164
|
+
user = users[uid]
|
|
165
|
+
tokens = user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
|
|
166
|
+
if token in tokens:
|
|
167
|
+
user.attrs[FIREBASE_DEVICE_TOKENS] = [tok for tok in tokens if token != tok]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_device_tokens_for_user(login: str) -> List[str]:
|
|
171
|
+
users = ugm_backend.ugm.users
|
|
172
|
+
if login not in users:
|
|
173
|
+
uid = users.id_for_login(login)
|
|
174
|
+
else:
|
|
175
|
+
uid = login
|
|
176
|
+
|
|
177
|
+
if uid in users:
|
|
178
|
+
user = users[uid]
|
|
179
|
+
return user.attrs.get(FIREBASE_DEVICE_TOKENS, []) or []
|
|
180
|
+
else:
|
|
181
|
+
return []
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firebase Messaging with cone
|
|
3
|
+
|
|
4
|
+
docs see:
|
|
5
|
+
https://firebase.google.com/docs/cloud-messaging/send-message
|
|
6
|
+
"""
|
|
7
|
+
from cone.app import ugm_backend
|
|
8
|
+
from cone.firebase.management import get_device_tokens_for_user
|
|
9
|
+
from cone.firebase.management import unregister_device_token_for_user
|
|
10
|
+
from firebase_admin import messaging
|
|
11
|
+
from firebase_admin.exceptions import InvalidArgumentError
|
|
12
|
+
from firebase_admin.messaging import UnregisteredError
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import Dict
|
|
15
|
+
from typing import List
|
|
16
|
+
import firebase_admin
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger('cone.firebase')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def send_message(data: Dict[str, Any], token: str) -> str:
|
|
24
|
+
messaging = firebase_admin.messaging
|
|
25
|
+
message = messaging.Message(data=data, token=token)
|
|
26
|
+
response = messaging.send(message)
|
|
27
|
+
return response
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def send_messages(data: Dict[str, Any], tokens: List[str]):
|
|
31
|
+
messaging = firebase_admin.messaging
|
|
32
|
+
message = messaging.MulticastMessage(data=data, tokens=tokens)
|
|
33
|
+
response = messaging.send_multicast(message)
|
|
34
|
+
return response
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def send_message_to_user(login: str, data: Dict[str, Any]) -> List[str]:
|
|
38
|
+
tokens = get_device_tokens_for_user(login)
|
|
39
|
+
results = []
|
|
40
|
+
# return send_messages(data, tokens)
|
|
41
|
+
for token in tokens:
|
|
42
|
+
try:
|
|
43
|
+
res = send_message(data, token)
|
|
44
|
+
results.append(res)
|
|
45
|
+
except (UnregisteredError, InvalidArgumentError) as ex:
|
|
46
|
+
unregister_device_token_for_user(login, token)
|
|
47
|
+
logger.exception(f"error sending message to:{token}. the token is not registered in firebase, will be automatically unregistered")
|
|
48
|
+
except Exception as ex:
|
|
49
|
+
logger.exception(f"message to token {token} failed")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
return results
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from cone import firebase
|
|
2
|
+
from cone.app.ugm import ugm_backend
|
|
3
|
+
from cone.firebase import api
|
|
4
|
+
from cone.firebase import authentication
|
|
5
|
+
from cone.firebase import messaging
|
|
6
|
+
from cone.firebase.testing import firebase_admin
|
|
7
|
+
from cone.firebase.testing.firebase_admin import messaging as fb_fake_messaging
|
|
8
|
+
from cone.ugm.events import UserCreatedEvent
|
|
9
|
+
from cone.ugm.events import UserDeletedEvent
|
|
10
|
+
from cone.ugm.events import UserModifiedEvent
|
|
11
|
+
from cone.ugm.testing import localmanager_config
|
|
12
|
+
from cone.ugm.testing import ugm_config
|
|
13
|
+
from cone.ugm.testing import UGMLayer
|
|
14
|
+
from zope.event import classhandler
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
service_account_json = {
|
|
22
|
+
'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
|
|
23
|
+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
|
|
24
|
+
'client_email': 'firebase-adminsdk-foo@example.iam.gserviceaccount.com',
|
|
25
|
+
'client_id': 'xxx',
|
|
26
|
+
'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo@example.iam.gserviceaccount.com',
|
|
27
|
+
'private_key': '-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----\n',
|
|
28
|
+
'private_key_id': 'xxx',
|
|
29
|
+
'project_id': 'example',
|
|
30
|
+
'token_uri': 'https://oauth2.googleapis.com/token',
|
|
31
|
+
'type': 'service_account'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fake_sign_in_with_email_and_password(
|
|
36
|
+
email,
|
|
37
|
+
password,
|
|
38
|
+
api_key,
|
|
39
|
+
return_secure_token=True
|
|
40
|
+
):
|
|
41
|
+
try:
|
|
42
|
+
res = firebase_admin.get_user_by_email(email)
|
|
43
|
+
res['kind'] = 'identitytoolkit#VerifyPasswordResponse'
|
|
44
|
+
except KeyError:
|
|
45
|
+
res = {
|
|
46
|
+
'error': {
|
|
47
|
+
'code': 400,
|
|
48
|
+
'message': 'INVALID_EMAIL',
|
|
49
|
+
'errors': [{
|
|
50
|
+
'message': 'INVALID_EMAIL',
|
|
51
|
+
'domain': 'global',
|
|
52
|
+
'reason': 'invalid'
|
|
53
|
+
}]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return res
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FirebaseLayer(UGMLayer):
|
|
60
|
+
|
|
61
|
+
def make_app(self):
|
|
62
|
+
ugm_users_file = os.path.join(self.ugm_dir, 'users')
|
|
63
|
+
ugm_groups_file = os.path.join(self.ugm_dir, 'groups')
|
|
64
|
+
ugm_roles_file = os.path.join(self.ugm_dir, 'roles')
|
|
65
|
+
ugm_datadir = os.path.join(self.ugm_dir, 'data')
|
|
66
|
+
# call direct to grandparent
|
|
67
|
+
super(UGMLayer, self).make_app(**{
|
|
68
|
+
'cone.plugins': '\n'.join([
|
|
69
|
+
'cone.ugm',
|
|
70
|
+
'cone.firebase'
|
|
71
|
+
]),
|
|
72
|
+
'ugm.backend': 'file',
|
|
73
|
+
'ugm.config': ugm_config,
|
|
74
|
+
'ugm.localmanager_config': localmanager_config,
|
|
75
|
+
'ugm.users_file': ugm_users_file,
|
|
76
|
+
'ugm.groups_file': ugm_groups_file,
|
|
77
|
+
'ugm.roles_file': ugm_roles_file,
|
|
78
|
+
'ugm.datadir': ugm_datadir,
|
|
79
|
+
'firebase.web_api_key': 'xxxxxxxxxx',
|
|
80
|
+
'firebase.service_account_json_file': os.path.join(
|
|
81
|
+
self.tempdir,
|
|
82
|
+
'service_account.json'
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
ugm_backend.initialize()
|
|
86
|
+
|
|
87
|
+
def setUp(self, args=None):
|
|
88
|
+
self.patch_modules()
|
|
89
|
+
self.tempdir = tempfile.mkdtemp()
|
|
90
|
+
with open(os.path.join(self.tempdir, 'service_account.json'), 'w') as f:
|
|
91
|
+
f.write(json.dumps(service_account_json))
|
|
92
|
+
super(FirebaseLayer, self).setUp()
|
|
93
|
+
# create a firebase only user
|
|
94
|
+
firebase_admin.create_user(id="donald", login="email", email="donald@duck.com")
|
|
95
|
+
# create a local only user
|
|
96
|
+
users = ugm_backend.ugm.users
|
|
97
|
+
users.create(
|
|
98
|
+
"donald_local",
|
|
99
|
+
login="email",
|
|
100
|
+
email="donald_local@duck.com",
|
|
101
|
+
fullname="Donald Duck local",
|
|
102
|
+
)
|
|
103
|
+
users["donald_local"].passwd(None, "daisy1")
|
|
104
|
+
assert "donald_local" in users
|
|
105
|
+
|
|
106
|
+
def tearDown(self):
|
|
107
|
+
self.unpatch_modules()
|
|
108
|
+
self.unregister_handlers()
|
|
109
|
+
super(FirebaseLayer, self).tearDown()
|
|
110
|
+
shutil.rmtree(self.tempdir)
|
|
111
|
+
firebase_admin.delete_user("donald")
|
|
112
|
+
|
|
113
|
+
def patch_modules(self):
|
|
114
|
+
self.firebase_admin_orgin = firebase.firebase_admin
|
|
115
|
+
firebase.firebase_admin = firebase_admin
|
|
116
|
+
firebase.firebase_admin.messaging = fb_fake_messaging
|
|
117
|
+
api.firebase_admin = firebase_admin
|
|
118
|
+
messaging.firebase_admin = firebase_admin
|
|
119
|
+
self.sign_in_with_email_and_password = authentication.sign_in_with_email_and_password
|
|
120
|
+
authentication.sign_in_with_email_and_password = fake_sign_in_with_email_and_password
|
|
121
|
+
|
|
122
|
+
def unpatch_modules(self):
|
|
123
|
+
firebase.firebase_admin = self.firebase_admin_orgin
|
|
124
|
+
api.firebase_admin = self.firebase_admin_orgin
|
|
125
|
+
authentication.sign_in_with_email_and_password = self.sign_in_with_email_and_password
|
|
126
|
+
|
|
127
|
+
def unregister_handlers(self):
|
|
128
|
+
del classhandler.registry[UserCreatedEvent]
|
|
129
|
+
del classhandler.registry[UserDeletedEvent]
|
|
130
|
+
del classhandler.registry[UserModifiedEvent]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
firebase_layer = FirebaseLayer()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# faking the firebase auth api
|
|
2
|
+
|
|
3
|
+
from . import credentials # noqua
|
|
4
|
+
from . import messaging # noqa
|
|
5
|
+
|
|
6
|
+
_apps = None
|
|
7
|
+
|
|
8
|
+
users_by_email = {}
|
|
9
|
+
users_by_id = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def delete_user(uid):
|
|
13
|
+
user = get_user(uid)
|
|
14
|
+
del users_by_email[user["email"]]
|
|
15
|
+
del users_by_id[uid]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user(uid):
|
|
19
|
+
return users_by_id[uid]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_user_by_email(email):
|
|
23
|
+
return users_by_email[email]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def update_user(uid, **kw):
|
|
27
|
+
user = users_by_id[uid]
|
|
28
|
+
user.update(**kw)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_user(**kw):
|
|
32
|
+
id = kw.pop("id")
|
|
33
|
+
email = kw.pop("email")
|
|
34
|
+
if not id:
|
|
35
|
+
id = email
|
|
36
|
+
|
|
37
|
+
new_user = {
|
|
38
|
+
"localId": id,
|
|
39
|
+
"email": email,
|
|
40
|
+
"idToken": "xxx",
|
|
41
|
+
"profilePicture": "",
|
|
42
|
+
"refreshToken": "string",
|
|
43
|
+
"expiresIn": "",
|
|
44
|
+
"registered":True
|
|
45
|
+
}
|
|
46
|
+
new_user.update(**kw)
|
|
47
|
+
users_by_id[new_user["localId"]] = new_user
|
|
48
|
+
users_by_email[new_user["email"]] = new_user
|
|
49
|
+
return new_user
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def initialize_app(cred):
|
|
53
|
+
global _apps
|
|
54
|
+
_apps = True
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Fake api for cloud messaging
|
|
2
|
+
|
|
3
|
+
class Message:
|
|
4
|
+
"""A message that can be sent via Firebase Cloud Messaging.
|
|
5
|
+
|
|
6
|
+
Contains payload information as well as recipient information. In particular, the message must
|
|
7
|
+
contain exactly one of token, topic or condition fields.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
|
11
|
+
strings.
|
|
12
|
+
notification: An instance of ``messaging.Notification`` (optional).
|
|
13
|
+
android: An instance of ``messaging.AndroidConfig`` (optional).
|
|
14
|
+
webpush: An instance of ``messaging.WebpushConfig`` (optional).
|
|
15
|
+
apns: An instance of ``messaging.ApnsConfig`` (optional).
|
|
16
|
+
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
|
|
17
|
+
token: The registration token of the device to which the message should be sent (optional).
|
|
18
|
+
topic: Name of the FCM topic to which the message should be sent (optional). Topic name
|
|
19
|
+
may contain the ``/topics/`` prefix.
|
|
20
|
+
condition: The FCM condition to which the message should be sent (optional).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None,
|
|
24
|
+
fcm_options=None, token=None, topic=None, condition=None):
|
|
25
|
+
self.data = data
|
|
26
|
+
self.notification = notification
|
|
27
|
+
self.android = android
|
|
28
|
+
self.webpush = webpush
|
|
29
|
+
self.apns = apns
|
|
30
|
+
self.fcm_options = fcm_options
|
|
31
|
+
self.token = token
|
|
32
|
+
self.topic = topic
|
|
33
|
+
self.condition = condition
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
return json.dumps(self, cls=MessageEncoder, sort_keys=True)
|
|
37
|
+
|
|
38
|
+
class MulticastMessage:
|
|
39
|
+
"""A message that can be sent to multiple tokens via Firebase Cloud Messaging.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
tokens: A list of registration tokens of targeted devices.
|
|
43
|
+
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
|
44
|
+
strings.
|
|
45
|
+
notification: An instance of ``messaging.Notification`` (optional).
|
|
46
|
+
android: An instance of ``messaging.AndroidConfig`` (optional).
|
|
47
|
+
webpush: An instance of ``messaging.WebpushConfig`` (optional).
|
|
48
|
+
apns: An instance of ``messaging.ApnsConfig`` (optional).
|
|
49
|
+
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None,
|
|
52
|
+
fcm_options=None):
|
|
53
|
+
# _Validators.check_string_list('MulticastMessage.tokens', tokens)
|
|
54
|
+
if len(tokens) > 500:
|
|
55
|
+
raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.')
|
|
56
|
+
self.tokens = tokens
|
|
57
|
+
self.data = data
|
|
58
|
+
self.notification = notification
|
|
59
|
+
self.android = android
|
|
60
|
+
self.webpush = webpush
|
|
61
|
+
self.apns = apns
|
|
62
|
+
self.fcm_options = fcm_options
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BatchResponse:
|
|
66
|
+
"""The response received from a batch request to the FCM API."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, responses):
|
|
69
|
+
self._responses = responses
|
|
70
|
+
self._success_count = len([resp for resp in responses if resp.success])
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def responses(self):
|
|
74
|
+
"""A list of ``messaging.SendResponse`` objects (possibly empty)."""
|
|
75
|
+
return self._responses
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def success_count(self):
|
|
79
|
+
return self._success_count
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def failure_count(self):
|
|
83
|
+
return len(self.responses) - self.success_count
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SendResponse:
|
|
87
|
+
"""The response received from an individual batched request to the FCM API."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, resp, exception):
|
|
90
|
+
self._exception = exception
|
|
91
|
+
self._message_id = None
|
|
92
|
+
if resp:
|
|
93
|
+
self._message_id = resp.get('name', None)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def message_id(self):
|
|
97
|
+
"""A message ID string that uniquely identifies the message."""
|
|
98
|
+
return self._message_id
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def success(self):
|
|
102
|
+
"""A boolean indicating if the request was successful."""
|
|
103
|
+
return self._message_id is not None and not self._exception
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def exception(self):
|
|
107
|
+
"""A ``FirebaseError`` if an error occurs while sending the message to the FCM service."""
|
|
108
|
+
return self._exception
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def send(message: Message, dry_run=False):
|
|
112
|
+
return 'projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def send_multicast(message: Message, dry_run=False):
|
|
116
|
+
return BatchResponse(
|
|
117
|
+
[
|
|
118
|
+
SendResponse(
|
|
119
|
+
dict(name='projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'), None)
|
|
120
|
+
]
|
|
121
|
+
)
|
cone/firebase/tests.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from cone import firebase
|
|
2
|
+
from cone.app.ugm import ugm_backend
|
|
3
|
+
from cone.firebase import messaging
|
|
4
|
+
from cone.firebase import testing
|
|
5
|
+
from cone.firebase.api import get_device_tokens_for_user
|
|
6
|
+
from cone.firebase.api import register_device_token_for_user
|
|
7
|
+
from cone.firebase.testing.firebase_admin import messaging as fb_fake_messaging
|
|
8
|
+
from node.tests import NodeTestCase
|
|
9
|
+
import sys
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
EXAMPLE_DEVICE_TOKEN = (
|
|
14
|
+
'dtt9cGrcSXicn8mW0tvcTQ:APA91bHlcidOIQwXoXVa3p22fBDvgeu'
|
|
15
|
+
'1kUwElEKpdVcliODGAbtjviOV7Ruls2h__enWF1P_gZApIVOOfHKGl'
|
|
16
|
+
'Tft0vWuzzwGapsXbZIIH9s7-rbpilV4Hu_JzoLBYAwpCoP3Nkf3foPv'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestFirebase(NodeTestCase):
|
|
21
|
+
layer = testing.firebase_layer
|
|
22
|
+
|
|
23
|
+
def test_initialize_firebase(self):
|
|
24
|
+
self.assertIsInstance(firebase.config, firebase.FirebaseConfig)
|
|
25
|
+
self.assertEqual(firebase.config.web_api_key, 'xxxxxxxxxx')
|
|
26
|
+
self.assertEqual(
|
|
27
|
+
firebase.config.service_account_json,
|
|
28
|
+
testing.service_account_json
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_firebase_authentication(self):
|
|
32
|
+
"""Tests a login situation where only a firebase user exists and after
|
|
33
|
+
authentication is added to the local UGM database.
|
|
34
|
+
"""
|
|
35
|
+
from cone.app import security
|
|
36
|
+
users = ugm_backend.ugm.users
|
|
37
|
+
request = self.layer.new_request()
|
|
38
|
+
security.AUTHENTICATOR = "firebase"
|
|
39
|
+
security.authenticate(request, "donald@duck.com", "daisy1")
|
|
40
|
+
|
|
41
|
+
# during the authentication the user should be added to UGM
|
|
42
|
+
self.assertTrue("donald" in users)
|
|
43
|
+
|
|
44
|
+
def test_local_authentication(self):
|
|
45
|
+
"""Tests a local only login for the situation that a certain user is
|
|
46
|
+
only given locally and login should the fall back to standard auth
|
|
47
|
+
"""
|
|
48
|
+
from cone.app import security
|
|
49
|
+
users = ugm_backend.ugm.users
|
|
50
|
+
self.assertTrue("donald_local" in users)
|
|
51
|
+
|
|
52
|
+
request = self.layer.new_request()
|
|
53
|
+
security.AUTHENTICATOR = "firebase"
|
|
54
|
+
security.authenticate(request, "donald_local", "daisy1")
|
|
55
|
+
|
|
56
|
+
def test_management(self):
|
|
57
|
+
"""Test management of device tokens for firebase messaging
|
|
58
|
+
"""
|
|
59
|
+
register_device_token_for_user("donald", EXAMPLE_DEVICE_TOKEN)
|
|
60
|
+
self.assertTrue(EXAMPLE_DEVICE_TOKEN in get_device_tokens_for_user("donald"))
|
|
61
|
+
|
|
62
|
+
def test_send_message(self):
|
|
63
|
+
registration_token = EXAMPLE_DEVICE_TOKEN
|
|
64
|
+
message = fb_fake_messaging.Message(
|
|
65
|
+
data={
|
|
66
|
+
'score': '850',
|
|
67
|
+
'time': '2:45',
|
|
68
|
+
},
|
|
69
|
+
token=registration_token,
|
|
70
|
+
)
|
|
71
|
+
res = fb_fake_messaging.send(message, dry_run=True)
|
|
72
|
+
assert res == 'projects/willholzen-293208/messages/0:1612781129630326%d758af2bf9fd7ecd'
|
|
73
|
+
|
|
74
|
+
def test_send_multicast_message(self):
|
|
75
|
+
registration_tokens = [EXAMPLE_DEVICE_TOKEN]
|
|
76
|
+
message = fb_fake_messaging.MulticastMessage(
|
|
77
|
+
data={'score': '850', 'time': '2:45'},
|
|
78
|
+
tokens=registration_tokens,
|
|
79
|
+
)
|
|
80
|
+
fb_fake_messaging.send_multicast(message)
|
|
81
|
+
|
|
82
|
+
def test_send_message_to_user(self):
|
|
83
|
+
data = {'score': '850', 'time': '2:45'}
|
|
84
|
+
res = messaging.send_message_to_user("donald", data)
|
|
85
|
+
self.assertTrue(res[0].startswith('projects/willholzen-293208/messages/'))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_tests():
|
|
89
|
+
from cone.firebase import tests
|
|
90
|
+
from zope.testrunner.runner import Runner
|
|
91
|
+
|
|
92
|
+
suite = unittest.TestSuite()
|
|
93
|
+
suite.addTest(unittest.findTestCases(tests))
|
|
94
|
+
|
|
95
|
+
runner = Runner(found_suites=[suite])
|
|
96
|
+
runner.run()
|
|
97
|
+
sys.exit(int(runner.failed))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# def test_run_tests():
|
|
101
|
+
# from cone.firebase import tests
|
|
102
|
+
# from zope.testrunner.runner import Runner
|
|
103
|
+
#
|
|
104
|
+
# suite = unittest.TestSuite()
|
|
105
|
+
# suite.addTest(unittest.findTestCases(tests))
|
|
106
|
+
#
|
|
107
|
+
# runner = Runner(found_suites=[suite])
|
|
108
|
+
# runner.run()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
run_tests()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('cone',));importlib = __import__('importlib.util');__import__('importlib.machinery');m = sys.modules.setdefault('cone', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('cone', [os.path.dirname(p)])));m = m or sys.modules.setdefault('cone', types.ModuleType('cone'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cone.firebase
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Firebase integration for cone.app
|
|
5
|
+
Home-page: http://github.com/conestack/cone.firebase
|
|
6
|
+
Author: Cone Contributors
|
|
7
|
+
Author-email: dev@conestack.org
|
|
8
|
+
License: Simplified BSD
|
|
9
|
+
Keywords: node pyramid cone web
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
13
|
+
License-File: LICENSE.rst
|
|
14
|
+
Requires-Dist: proto-plus==1.18.1
|
|
15
|
+
Requires-Dist: google-api-core==1.26.3
|
|
16
|
+
Requires-Dist: google-api-python-client==2.2.0
|
|
17
|
+
Requires-Dist: google-auth==1.29.0
|
|
18
|
+
Requires-Dist: google-auth-httplib2==0.1.0
|
|
19
|
+
Requires-Dist: google-auth-oauthlib==0.4.6
|
|
20
|
+
Requires-Dist: google-cloud-core==1.6.0
|
|
21
|
+
Requires-Dist: google-cloud-firestore==2.1.0
|
|
22
|
+
Requires-Dist: google-cloud-storage==1.37.1
|
|
23
|
+
Requires-Dist: google-crc32c==1.1.2
|
|
24
|
+
Requires-Dist: google-pasta==0.2.0
|
|
25
|
+
Requires-Dist: google-resumable-media==1.2.0
|
|
26
|
+
Requires-Dist: googleapis-common-protos==1.53.0
|
|
27
|
+
Requires-Dist: setuptools
|
|
28
|
+
Requires-Dist: cone.app<1.1.0
|
|
29
|
+
Requires-Dist: requests
|
|
30
|
+
Requires-Dist: firebase_admin==4.5.3
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: zope.testrunner; extra == "test"
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: provides-extra
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
.. image:: https://img.shields.io/pypi/v/cone.firebase.svg
|
|
46
|
+
:target: https://pypi.python.org/pypi/cone.firebase
|
|
47
|
+
:alt: Latest PyPI version
|
|
48
|
+
|
|
49
|
+
.. image:: https://img.shields.io/pypi/dm/cone.firebase.svg
|
|
50
|
+
:target: https://pypi.python.org/pypi/cone.firebase
|
|
51
|
+
:alt: Number of PyPI downloads
|
|
52
|
+
|
|
53
|
+
.. image:: https://travis-ci.org/bluedynamics/cone.firebase.svg?branch=master
|
|
54
|
+
:target: https://travis-ci.org/bluedynamics/cone.firebase
|
|
55
|
+
|
|
56
|
+
.. image:: https://coveralls.io/repos/github/bluedynamics/cone.firebase/badge.svg?branch=master
|
|
57
|
+
:target: https://coveralls.io/github/bluedynamics/cone.firebase?branch=master
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
This package provides a firebase integration in to cone.app.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Installation
|
|
64
|
+
------------
|
|
65
|
+
|
|
66
|
+
Include ``cone.firebase`` to install dependencies in your application's
|
|
67
|
+
``setup.py``.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Configuration
|
|
71
|
+
-------------
|
|
72
|
+
|
|
73
|
+
Adopt your application config ini file to define firebase related API keys.
|
|
74
|
+
|
|
75
|
+
.. code-block:: ini
|
|
76
|
+
|
|
77
|
+
[app:my_app]
|
|
78
|
+
use = egg:cone.app#main
|
|
79
|
+
|
|
80
|
+
cone.plugins =
|
|
81
|
+
cone.firebase
|
|
82
|
+
|
|
83
|
+
cone.authenticator = firebase
|
|
84
|
+
|
|
85
|
+
firebase.web_api_key = xxx
|
|
86
|
+
firebase.service_account_json_file = path/to/service_account.json
|
|
87
|
+
|
|
88
|
+
also in your ugm.xml you can optionally add the `firebase_user` checkbox:
|
|
89
|
+
|
|
90
|
+
.. code-block:: xml
|
|
91
|
+
|
|
92
|
+
<users_form_attrmap>
|
|
93
|
+
...
|
|
94
|
+
<elem>
|
|
95
|
+
<key>firebase_user</key>
|
|
96
|
+
<value>Firebase User</value>
|
|
97
|
+
</elem>
|
|
98
|
+
</users_form_attrmap>
|
|
99
|
+
|
|
100
|
+
When you want to upgrade an existing user to FB, you can do this by checking
|
|
101
|
+
`firebase_user` in the user edit form, butt attention: you have to manually
|
|
102
|
+
enter the password since the firebase API expects the password in cleartext.
|
|
103
|
+
|
|
104
|
+
Contributors
|
|
105
|
+
============
|
|
106
|
+
|
|
107
|
+
- Robert Niederreiter
|
|
108
|
+
- Phil Auersperg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
Changes
|
|
112
|
+
=======
|
|
113
|
+
|
|
114
|
+
0.1 (2025-10-25)
|
|
115
|
+
----------------
|
|
116
|
+
|
|
117
|
+
- Initial
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
License
|
|
121
|
+
=======
|
|
122
|
+
|
|
123
|
+
Copyright (c) 2021, BlueDynamics Alliance, Austria
|
|
124
|
+
Copyright (c) 2021, Cone Contributors
|
|
125
|
+
All rights reserved.
|
|
126
|
+
|
|
127
|
+
Redistribution and use in source and binary forms, with or without
|
|
128
|
+
modification, are permitted provided that the following conditions are met:
|
|
129
|
+
|
|
130
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
131
|
+
list of conditions and the following disclaimer.
|
|
132
|
+
|
|
133
|
+
* Redistributions in binary form must reproduce the above copyright notice, this
|
|
134
|
+
list of conditions and the following disclaimer in the documentation and/or
|
|
135
|
+
other materials provided with the distribution.
|
|
136
|
+
|
|
137
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
138
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
139
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
140
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
141
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
142
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
143
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
144
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
145
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
146
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
cone.firebase-0.1-py3.9-nspkg.pth,sha256=WmSYeXqiFe1Qae-WQFmtUgaaYvduormqDUjrRgs1ASs,457
|
|
2
|
+
cone/firebase/__init__.py,sha256=8Y5bVP_QZ-wPY8raovTb_df_cVEmOn5Gr0Lt5x76MfU,2242
|
|
3
|
+
cone/firebase/api.py,sha256=XGsOZOnDwKkO5_SjNrUiKPPSE4ptTLZY0hv5v5wyEkw,328
|
|
4
|
+
cone/firebase/authentication.py,sha256=e1bBvMBodSngpoXBM9_6t2_XEq8qOWKRXB3l0bxU16w,2433
|
|
5
|
+
cone/firebase/management.py,sha256=EmbqLIJ0W57EHKG96SN_f7FgFwIygCegmhlr398bMs4,6044
|
|
6
|
+
cone/firebase/messaging.py,sha256=9_HJ5rd60EZr-6LgubMi1B_1i_P_k51XD5Xl4JAlyXM,1733
|
|
7
|
+
cone/firebase/tests.py,sha256=255kdmM2cwa8Rt2RCCtP316i_ipLnANLElnV0QQniY8,3830
|
|
8
|
+
cone/firebase/testing/__init__.py,sha256=GFML_Gqtfw73XlAkaBdtXNMz1kL_BhLeuBkv2ivZ9Uk,4838
|
|
9
|
+
cone/firebase/testing/firebase_admin/__init__.py,sha256=VaekqatvJ2iE1M0H0-mq8rREGf22847nMzYTzTDk72E,983
|
|
10
|
+
cone/firebase/testing/firebase_admin/credentials.py,sha256=dWr3nlUtuiFuGN4WaqocNvbxjuS5CSw5KHxwutSOHws,131
|
|
11
|
+
cone/firebase/testing/firebase_admin/messaging.py,sha256=l09gMD02BZvpxMrwaw37vhfZ4UWLfiFJq-lGFdbZVHI,4589
|
|
12
|
+
cone_firebase-0.1.dist-info/licenses/LICENSE.rst,sha256=zaJbEi2UOhNjFLC1d2qra_JVlZceavFtt9yOXoM37Ng,1368
|
|
13
|
+
cone_firebase-0.1.dist-info/METADATA,sha256=6Ghw_bY-Lm4UMjdIXZ0DYiSSQgRRllfhvHDG1kza8_0,4527
|
|
14
|
+
cone_firebase-0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
cone_firebase-0.1.dist-info/namespace_packages.txt,sha256=lu11VEz--UhjAtw-HG4CejJNFf-TZlq7z1O6wZ7Vrjk,5
|
|
16
|
+
cone_firebase-0.1.dist-info/top_level.txt,sha256=lu11VEz--UhjAtw-HG4CejJNFf-TZlq7z1O6wZ7Vrjk,5
|
|
17
|
+
cone_firebase-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
License
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2021, BlueDynamics Alliance, Austria
|
|
5
|
+
Copyright (c) 2021, Cone Contributors
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source and binary forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
12
|
+
list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
* Redistributions in binary form must reproduce the above copyright notice, this
|
|
15
|
+
list of conditions and the following disclaimer in the documentation and/or
|
|
16
|
+
other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
22
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
25
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cone
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cone
|