I am currently developing an android chat app. I am very new to Android Studio, JWT Token Authorization, and Django Rest Framework. Right now I am having issue to work on the Django side.
So basically I was setting up a login page from my Android, and I want it to login using phone number and password as the needed credentials. However, I also want to use JWT Token Auth to make my application more secure.
Currently I have my project urls.py pointing to one of the JWT Token API
urls.py
from django.contrib import admin
from django.urls import path,include
from django.conf.urls import include, url
from rest_framework_simplejwt import views as jwt_views
urlpatterns = [
path('admin/', admin.site.urls),
path('account/',include('restaccount.urls')) ,
path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]
This would lead to the server page which was
*PS : The phone number fields should be the default username field..(I have made some trial modifications on my code prior I post this).
I also have set up a models that was inherit from AbstractUser
models.py
class RegisterUser(AbstractUser):
phone_number = PhoneField(name='phone_number',unique=True)
birthday = models.DateField(name ='birthday',null= True)
nickname = models.CharField(max_length=100,name = 'nickname')
def __str__(self):
return self.phone_number
Currently I have tried to make a lot of modifications to my model, like :
change username = None
REQUIRED_FIELDS = []
USERNAME_FIELDS = 'phone_number'
I realize that the Token Obtain Pair View is following the Django Administration page in terms of the information that you needed (username and password).
However when I modified, I try to create superuser and try to login too Django Admin with my modified data..But I still cannot log in.. Also, I try to get token from the superuser that I have made, but it will response in "detail": "No active account found with the given credentials"
Can somebody enlighten me of the steps that I should take now?? I have tried to look for solutions but none of them solve my problem
Here's the point TLDR:
I want my app to Login using phone number and password and want to use JWT Token Auth to make it secure.
I realize the ObtainTokenPair view follows Django Admin credentials, so I have tried to modify my backend to be "log in" using phone number and password.
After I modified, I can't login to Django Admin and cannot get token with the superuser I created.
Here some of the related file attach:
Settings.py
"""
Django settings for androidapp project.
Generated by 'django-admin startproject' using Django 3.0.7.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '6qdk058^8b2#-pnw!cr1pbd(sao)vj+v69&4874zjh95xu7pg)'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['172.31.120.211',]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'restaccount',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'androidapp.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'androidapp.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'Orbital',
'USER' :'SomeUser',
'PASSWORD':'Pass',
'HOST' : 'localhost',
'PORT' : '',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = False
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
AUTH_USER_MODEL = 'restaccount.RegisterUser'
#FORMAT FOR DATE INPUT
DATE_INPUT_FORMATS = ('%d-%m-%Y', '%d/%m/%Y', '%d/%m/%y', '%d %b %Y',
'%d %b, %Y', '%d %b %Y', '%d %b, %Y', '%d %B, %Y',
'%d %B %Y')
#Format for date-time input format
DATETIME_INPUT_FORMATS = ('%d/%m/%Y %H:%M:%S', '%d/%m/%Y %H:%M', '%d/%m/%Y',
'%d/%m/%y %H:%M:%S', '%d/%m/%y %H:%M', '%d/%m/%y',
'%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%d')
# Adding REST_FRAMEWORK SETTING WITH JWT AUTHENTICATION
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
# AUTHENTICATION_BACKENDS = (
# 'django.contrib.auth.backends.ModelBackend',
# 'restaccount.backends.UserBackend'
# )
RegisterUserManager inside models.py
class RegisterUserManager(BaseUserManager):
def create_user(self, phone_number,password, **extra_fields):
if not phone_number:
raise ValueError('The phone number must be set')
user = self.model(
phone_number=phone_number,
password = password,
**extra_fields)
user.save()
return user
def create_superuser(self,phone_number,password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
# print(phone_number)
if extra_fields.get('is_staff') is not True:
raise ValueError(_('Superuser must have is_staff=True.'))
if extra_fields.get('is_superuser') is not True:
raise ValueError(_('Superuser must have is_superuser=True.'))
return self.create_user(phone_number, password,**extra_fields)
Its quite difficult to pin point the bug without getting hands-on to the actual project. I can't not find the bug or fix your project. that you have to do on your own. But I can surely share what I think would help you avoid bug and fix your project.
what I could understand
you want a custom user model
your want to use jwt authentication
so, let's begin. User model and authentication are two different things. create Custom User model first.
firstly, remove all users from database
create Custom User model following this 'A full example' exactly (check by creating superuser if custom user model is working properly, if not that means you missed something try again)
If you have successfully created custom user model that means you now have substituted 'username' with 'phone number' (in your case)
for authentication you can use custom authentication or as you tried you can use existing packages. Configure it to act as default authentication backend.
your choice of authentication package should take username and password, check if there is a user match those credentials create a token and return that token. you don't need to modify the authentication process you just provide username field(phone number) and password. Now here you might need to do something like
{username: phone_number, password: password}
because your authentication package might not support custom user.
hope it helps.
Related
I want to verify user profile from Google Authorization code sent by android client, to do that, we have to download client_secrets.json and put it inside our rails app. Just like this tutorial https://developers.google.com/identity/protocols/OAuth2WebServer
But when I try to follow this step
require 'google/apis/drive_v2'
require 'google/api_client/client_secrets'
client_secrets = Google::APIClient::ClientSecrets.load
auth_client = client_secrets.to_authorization
auth_client.update!(
:scope => 'https://www.googleapis.com/auth/drive.metadata.readonly',
:redirect_uri => 'http://www.example.com/oauth2callback',
:additional_parameters => {
"access_type" => "offline", # offline access
"include_granted_scopes" => "true" # incremental auth
}
)
Rails throws an error said "No client_secrets.json filename supplied and/or could not be found in search path."
The errors shows up even though I have insert client_secrets.json inside config/client_secrets.json
Do you know what's the problem or what's the alternative for this solution, thank you.
It seems, ClientSecrets.load accepts optional argument which is config filename path. So I believe it's okay if you specify filename directly:
Google::APIClient::ClientSecrets.load("#{Rails.root}/config/client_secrets.json")
I am using oauth 2.0 with hybrid flow for google login https://developers.google.com/identity/sign-in/web/server-side-flow in my Android app. I get the one time authorization code into the android app and post it to my flask api through postman. When I apply flow.step2_exchange to this one time auth code in the api it gives me flow exchange error. I have checked the auth code that arrives in the api is same as I got in my app. I cannot find the reason for the error.
My one time auth code looks like this :4/qXilPdy7xOVe5swCBlVRrxjuVu8zEzfcmidlooo7_ls
Code Snippet of my flask api:
# IMPORTS FOR THIS STEP
from oauth2client.client import flow_from_clientsecrets
from oauth2client.client import FlowExchangeError
import httplib2
import json
from flask import make_response
import requests
app = Flask(__name__)
CLIENT_ID = json.loads(
open('client_secrets.json', 'r').read())['web']['client_id']
APPLICATION_NAME = "OAUTH_SERVER"
SCOPES = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
# Add other requested scopes.
]
# Connect to Database and create database session
engine = create_engine('sqlite:///restaurantmenu.db')
Base.metadata.bind = engine
DBSession = sessionmaker(bind=engine)
session = DBSession()
#app.route('/gconnect', methods=['POST'])
def gconnect():
request.get_data()
code = request.data.decode('utf-8')
print (code)
# Upgrade the authorization code into a credentials object
oauth_flow = flow_from_clientsecrets('client_secrets.json', scope = SCOPES)
oauth_flow.redirect_uri = 'postmessage'
try:
credentials = oauth_flow.step2_exchange(code)
if credentials is None:
print ("it is empty")
except FlowExchangeError:
response = make_response(
json.dumps('Failed to upgrade the authorization code.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
My client_secret.json for the Api is named OAUTH_SERVER and it as follows:
{"web":
{"client_id":"matches the one in console.apps.googleusercontent.com",
"project_id":"oauthapi",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"###########",
"redirect_uris["http://localhost:5000/gconnect","http://localhost:5000/"],
"javascript_origins":["http://localhost:5000"]}
}
As I see this is from the Udacity course - Authentication & Authorization. Please check if the login.html contains the proper data-clientid value. I had the same issue because upon copy forgot to change it with mine.
I think you need to change
# Upgrade the authorization code into a credentials object
oauth_flow = flow_from_clientsecrets('client_secrets.json', scope = SCOPES)
oauth_flow.redirect_uri = 'postmessage'
try:
credentials = oauth_flow.step2_exchange(code)
if credentials is None:
print ("it is empty")
except FlowExchangeError:
to
try:
# Upgrade the authorization code into a credentials object
oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='')
oauth_flow.redirect_uri = 'postmessage'
credentials = oauth_flow.step2_exchange(code)
except FlowExchangeError:
We are planning to move a chat server application resides on parse.com to Google app engine's datastore since parse is going to shutdown it's service on Jan 2017. I think this should be possible through App engine's XMPP API. Not sure, I love to hear from you..
Currently I'm testing with this code provided by Google
# Copyright 2009 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Crowdguru sample application using the XMPP service on Google App Engine."""
import datetime
from google.appengine.api import datastore_types
from google.appengine.api import xmpp
from google.appengine.ext import ndb
from google.appengine.ext.webapp import xmpp_handlers
import webapp2
from webapp2_extras import jinja2
PONDER_MSG = 'Hmm. Let me think on that a bit.'
TELLME_MSG = 'While I\'m thinking, perhaps you can answer me this: {}'
SOMEONE_ANSWERED_MSG = ('We seek those who are wise and fast. One out of two '
'is not enough. Another has answered my question.')
ANSWER_INTRO_MSG = 'You asked me: {}'
ANSWER_MSG = 'I have thought long and hard, and concluded: {}'
WAIT_MSG = ('Please! One question at a time! You can ask me another once you '
'have an answer to your current question.')
THANKS_MSG = 'Thank you for your wisdom.'
TELLME_THANKS_MSG = THANKS_MSG + ' I\'m still thinking about your question.'
EMPTYQ_MSG = 'Sorry, I don\'t have anything to ask you at the moment.'
HELP_MSG = ('I am the amazing Crowd Guru. Ask me a question by typing '
'\'/tellme the meaning of life\', and I will answer you forthwith! '
'To learn more, go to {}/')
MAX_ANSWER_TIME = 120
class IMProperty(ndb.StringProperty):
"""A custom property for handling IM objects.
IM or Instant Message objects include both an address and its protocol. The
constructor and __str__ method on these objects allow easy translation from
type string to type datastore_types.IM.
"""
def _validate(self, value):
"""Validator to make sure value is an instance of datastore_types.IM.
Args:
value: The value to be validated. Should be an instance of
datastore_types.IM.
Raises:
TypeError: If value is not an instance of datastore_types.IM.
"""
if not isinstance(value, datastore_types.IM):
raise TypeError('expected an IM, got {!r}'.format(value))
def _to_base_type(self, value):
"""Converts native type (datastore_types.IM) to datastore type (string).
Args:
value: The value to be converted. Should be an instance of
datastore_types.IM.
Returns:
String corresponding to the IM value.
"""
return str(value)
def _from_base_type(self, value):
"""Converts datastore type (string) to native type (datastore_types.IM).
Args:
value: The value to be converted. Should be a string.
Returns:
String corresponding to the IM value.
"""
return datastore_types.IM(value)
class Question(ndb.Model):
"""Model to hold questions that the Guru can answer."""
question = ndb.TextProperty(required=True)
asker = IMProperty(required=True)
asked = ndb.DateTimeProperty(required=True, auto_now_add=True)
suspended = ndb.BooleanProperty(required=True)
assignees = IMProperty(repeated=True)
last_assigned = ndb.DateTimeProperty()
answer = ndb.TextProperty(indexed=True)
answerer = IMProperty()
answered = ndb.DateTimeProperty()
#staticmethod
#ndb.transactional
def _try_assign(key, user, expiry):
"""Assigns and returns the question if it's not assigned already.
Args:
key: ndb.Key: The key of a Question to try and assign.
user: datastore_types.IM: The user to assign the question to.
expiry: datetime.datetime: The expiry date of the question.
Returns:
The Question object. If it was already assigned, no change is made.
"""
question = key.get()
if not question.last_assigned or question.last_assigned < expiry:
question.assignees.append(user)
question.last_assigned = datetime.datetime.now()
question.put()
return question
#classmethod
def assign_question(cls, user):
"""Gets an unanswered question and assigns it to a user to answer.
Args:
user: datastore_types.IM: The identity of the user to assign a
question to.
Returns:
The Question entity assigned to the user, or None if there are no
unanswered questions.
"""
question = None
while question is None or user not in question.assignees:
# Assignments made before this timestamp have expired.
expiry = (datetime.datetime.now()
- datetime.timedelta(seconds=MAX_ANSWER_TIME))
# Find a candidate question
query = cls.query(cls.answerer == None, cls.last_assigned < expiry)
# If a question has never been assigned, order by when it was asked
query = query.order(cls.last_assigned, cls.asked)
candidates = [candidate for candidate in query.fetch(2)
if candidate.asker != user]
if not candidates:
# No valid questions in queue.
break
# Try and assign it
question = cls._try_assign(candidates[0].key, user, expiry)
# Expire the assignment after a couple of minutes
return question
#ndb.transactional
def unassign(self, user):
"""Unassigns the given user from this question.
Args:
user: datastore_types.IM: The user who will no longer be answering
this question.
"""
question = self.key.get()
if user in question.assignees:
question.assignees.remove(user)
question.put()
#classmethod
def get_asked(cls, user):
"""Returns the user's outstanding asked question, if any.
Args:
user: datastore_types.IM: The identity of the user asking.
Returns:
An unanswered Question entity asked by the user, or None if there
are no unanswered questions.
"""
query = cls.query(cls.asker == user, cls.answer == None)
return query.get()
#classmethod
def get_answering(cls, user):
"""Returns the question the user is answering, if any.
Args:
user: datastore_types.IM: The identity of the user answering.
Returns:
An unanswered Question entity assigned to the user, or None if there
are no unanswered questions.
"""
query = cls.query(cls.assignees == user, cls.answer == None)
return query.get()
def bare_jid(sender):
"""Identify the user by bare jid.
See http://wiki.xmpp.org/web/Jabber_Resources for more details.
Args:
sender: String; A jabber or XMPP sender.
Returns:
The bare Jabber ID of the sender.
"""
return sender.split('/')[0]
class XmppHandler(xmpp_handlers.CommandHandler):
"""Handler class for all XMPP activity."""
def unhandled_command(self, message=None):
"""Shows help text for commands which have no handler.
Args:
message: xmpp.Message: The message that was sent by the user.
"""
message.reply(HELP_MSG.format(self.request.host_url))
def askme_command(self, message=None):
"""Responds to the /askme command.
Args:
message: xmpp.Message: The message that was sent by the user.
"""
im_from = datastore_types.IM('xmpp', bare_jid(message.sender))
currently_answering = Question.get_answering(im_from)
question = Question.assign_question(im_from)
if question:
message.reply(TELLME_MSG.format(question.question))
else:
message.reply(EMPTYQ_MSG)
# Don't unassign their current question until we've picked a new one.
if currently_answering:
currently_answering.unassign(im_from)
def text_message(self, message=None):
"""Called when a message not prefixed by a /cmd is sent to the XMPP bot.
Args:
message: xmpp.Message: The message that was sent by the user.
"""
im_from = datastore_types.IM('xmpp', bare_jid(message.sender))
question = Question.get_answering(im_from)
if question:
other_assignees = question.assignees
other_assignees.remove(im_from)
# Answering a question
question.answer = message.arg
question.answerer = im_from
question.assignees = []
question.answered = datetime.datetime.now()
question.put()
# Send the answer to the asker
xmpp.send_message([question.asker.address],
ANSWER_INTRO_MSG.format(question.question))
xmpp.send_message([question.asker.address],
ANSWER_MSG.format(message.arg))
# Send acknowledgement to the answerer
asked_question = Question.get_asked(im_from)
if asked_question:
message.reply(TELLME_THANKS_MSG)
else:
message.reply(THANKS_MSG)
# Tell any other assignees their help is no longer required
if other_assignees:
xmpp.send_message([user.address for user in other_assignees],
SOMEONE_ANSWERED_MSG)
else:
self.unhandled_command(message)
def tellme_command(self, message=None):
"""Handles /tellme requests, asking the Guru a question.
Args:
message: xmpp.Message: The message that was sent by the user.
"""
im_from = datastore_types.IM('xmpp', bare_jid(message.sender))
asked_question = Question.get_asked(im_from)
if asked_question:
# Already have a question
message.reply(WAIT_MSG)
else:
# Asking a question
asked_question = Question(question=message.arg, asker=im_from)
asked_question.put()
currently_answering = Question.get_answering(im_from)
if not currently_answering:
# Try and find one for them to answer
question = Question.assign_question(im_from)
if question:
message.reply(TELLME_MSG.format(question.question))
return
message.reply(PONDER_MSG)
class XmppPresenceHandler(webapp2.RequestHandler):
"""Handler class for XMPP status updates."""
def post(self, status):
"""POST handler for XMPP presence.
Args:
status: A string which will be either available or unavailable
and will indicate the status of the user.
"""
sender = self.request.get('from')
im_from = datastore_types.IM('xmpp', bare_jid(sender))
suspend = (status == 'unavailable')
query = Question.query(Question.asker == im_from,
Question.answer == None,
Question.suspended == (not suspend))
question = query.get()
if question:
question.suspended = suspend
question.put()
class LatestHandler(webapp2.RequestHandler):
"""Displays the most recently answered questions."""
#webapp2.cached_property
def jinja2(self):
"""Cached property holding a Jinja2 instance.
Returns:
A Jinja2 object for the current app.
"""
return jinja2.get_jinja2(app=self.app)
def render_response(self, template, **context):
"""Use Jinja2 instance to render template and write to output.
Args:
template: filename (relative to $PROJECT/templates) that we are
rendering.
context: keyword arguments corresponding to variables in template.
"""
rendered_value = self.jinja2.render_template(template, **context)
self.response.write(rendered_value)
def get(self):
"""Handler for latest questions page."""
query = Question.query(Question.answered > None).order(
-Question.answered)
self.render_response('latest.html', questions=query.fetch(20))
APPLICATION = webapp2.WSGIApplication([
('/', LatestHandler),
('/_ah/xmpp/message/chat/', XmppHandler),
('/_ah/xmpp/presence/(available|unavailable)/', XmppPresenceHandler),
], debug=True)
If a user selects another user whom he wants to chat with should call API url /_ah/xmpp/message/chat/ which automatically invokes XmppHandler handler.
('/_ah/xmpp/message/chat/', XmppHandler)
And my doubt is, If he post a message like foo on that particular chat does it automatically invokes text_message method exists in XmppHandler ? Is there we need to configure xmpp on client side also?
For the client api compatible and db migration, you can host your own parse-server.
There is a simple express project to use parse-server.
https://github.com/ParsePlatform/parse-server-example
They are lots of deploy guide to each cloud-platform
Google App Engine
Heroku and mLab
AWS and Elastic Beanstalk
Digital Ocean
NodeChef
Microsoft Azure
Pivotal Web Services
Back4app
Or you can host your nodejs server with your domain name.
If you want to do something different from parse, you can send a pull request to parse-server. LiveQuery is the extra function created by contributors.
For more details, see the link from Parse.com , github wiki, and community links.
Parse has provided detailed information regarding the migration
process and how to move our app from their serviers to a seperately
hosted mongoDB instance and cloud company. Parse suggests the
migration be made in two steps:
The database is migrated to a service like MongoLab or ObjectRocket.
The server be migrated to a cloud hosting company like AWS, Google App Engine, or Heroku.
Parse has also put suggested deadlines in place:
they suggest that by April 28, 2016 the database be migrated and by
July 28, 2016, the server be migrated properly.
This will give you ample time to work out any bugs and ensure your app functions properly with no downtime!
A Backend-as-a-Service like parse.com coupled two aspects of what’s known as a backend into one: the server and database. The server, which manipulates the database, performs queries, fetches information, and other work-intensive tasks, interacts with the database. The two work hand-in-hand to form a backend.
With Parse going away, we must deal with the server and database
seperately.
Parse has already provided detailed info and easy migration tool for database to Mongodb hosted on any cloud.
Also it is easy to setup the node based parse server on any cloud platform including Google App Engine:
The easiest way to get Parse server running on Google Cloud is to start with the sample out on GitHub.
I am building an Android application using Parse Server Example on Heroku as backend. I need Mailgun to send password reset emails from ParseUI class ParseLoginHelpFragment.
I haven't found an answer on how to make Mailgun work with Heroku/Parse Server. Here is my config on Heroku:
Also tried MAILGUN_SMTP_PORT 589 with the same result.
Appreciate if anyone can point out the error in my setup.
EDIT: I understand that I need to enter the Mailgun API key and some additional setup. I have tried doing that in the index.js file:
var server = ParseServer({
...otherOptions,
// Enable email verification
verifyUserEmails: true,
// The public URL of your app.
// This will appear in the link that is used to verify email addresses and reset passwords.
// Set the mount path as it is in serverURL
publicServerURL: 'https://example.com/parse',
// Your apps name. This will appear in the subject and body of the emails that are sent.
appName: 'Parse App',
// The email adapter
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
// The address that your emails come from
fromAddress: 'parse#example.com',
// Your domain from mailgun.com
domain: 'example.com',
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
}
});
However, the app crashes on Heroku, there is still something missing...
Finally got this working:
1) Make sure that your domain is verified on Mailgun by using the steps they provide.
2) Check the zone settings for your domain with your hosting provider. The exact instruction may vary depending on the provider, I use Bluehost, and these settings are in Domains>>Zone settings
3) Create a mail address at your hosting provider and enter the email and password into Mailgun settings for the domain.
4) Replace code for var api in index.js file:
var api = new ParseServer({
databaseURI: databaseUri || 'mongodb://localhost:27017/dev',
cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
appId: process.env.APP_ID || 'myAppId',
masterKey: process.env.MASTER_KEY || '', //Add your master key here. Keep it secret!
serverURL: process.env.SERVER_URL || 'http://localhost:1337/parse', // Don't forget to change to https if needed
liveQuery: {
classNames: ["Posts", "Comments"] // List of classes to support for query subscriptions
},
appName: 'Your App Name',
publicServerURL: 'https://yourappaddress.com/parse',
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
// The address that your emails come from
fromAddress: 'yourapp#yourappaddress.com',
// Your domain from mailgun.com
domain: 'email.yourappaddress.com',
// Your API key from mailgun.com
apiKey: 'key-private-key-from-mailgun',
}
}
});
If you don't use Heroku, you don't need to use process.env.*
Hope that helps
If you didn't already do this, you need to install the parse server simple mailgun adapter using npm. Just go to the root of your parse-server-example directory: npm install parse-server-simple-mailgun-adapter.
I'm developing a Sinatra app for which I'd like to use OmniAuth. So far, I have something similar to this for the web app:
http://codebiff.com/omniauth-with-sinatra
I'd like the web app to be usable via Android phones which would use an API, authenticating by means of a token. The development of an API seems to be covered nicely here:
Sinatra - API - Authentication
What is not clear is now I might arrange the login procedure. Presumably it would be along these lines:
User selects what service to use, e.g. Twitter, FaceBook &c., by means of an in-app button on the Android device.
The Android app opens a webview to log in to the web app.
A token is somehow created, stored in the web app's database, and returned to the Android app so that it can be stored and used for subsequent API requests.
I'm not very clear on how point 3 might be managed - does anyone have any suggestions?
As no-one seems to have any suggestions, here's what I've come up with so far. I don't think it's very good, though.
I've added an API key to the user model, which is created when the user is first authenticated:
class User
include DataMapper::Resource
property :id, Serial, :key => true
property :uid, String
property :name, String
property :nickname, String
property :created_at, DateTime
property :api_key, String, :key => true
end
....
get '/auth/:name/callback' do
auth = request.env["omniauth.auth"]
user = User.first_or_create({ :uid => auth["uid"]},
{ :uid => auth["uid"],
:nickname => auth["info"]["nickname"],
:name => auth["info"]["name"],
:api_key => SecureRandom.hex(20),
:created_at => Time.now })
session[:user_id] = user.id
session[:api_key] = user.api_key
flash[:info] = "Welcome, #{user.name}"
redirect "/success/#{user.id}/#{user.api_key}"
end
If the authorisation works then the api_key is supplied to the Android app, which will presumably store it on the device somewhere:
get '/success/:id/:api_key', :check => :valid_key? do
user = User.get(params[:id],params[:api_key])
if user.api_key == params[:api_key]
{'api_key' => user.api_key}.to_json
else
error 401
end
end
All API calls are protected as in the link in my original post:
register do
def check (name)
condition do
error 401 unless send(name) == true
end
end
end
helpers do
def valid_key?
user = User.first(:api_key => params[:api_key])
if !user.nil?
return true
end
return false
end
end
For public use I'll only allow SSL connections to the server. Any suggestions for improvement would be welcome.