So, I have the following code that all works in the browser but for some reason keeps redirecting me back to login on the actual Android device.
controllers.js
App.controller('LoginController', ['$scope', 'RequestService', '$location', 'OpenFB', '$rootScope', function($scope, RequestService, $location, OpenFB, $rootScope) {
$scope.provider = '';
$scope.loginFacebook = function() {
$scope.provider = 'facebook';
OpenFB.login('email, user_friends').then(
function () {
OpenFB.get('/me').success(function (user) {
localStorage.setItem('fbuser', JSON.stringify(user));
});
localStorage.setItem('provider', 'facebook');
RequestService.get($scope.baseUrl + 'user')
.success(function(data, status, headers, config){
$scope.user = data;
//Set our logged in var
localStorage.setItem('loggedIn', true);
// Check if the user has accepted EULA
if($scope.user.eula == 0) {
$location.path('/eula');
}
else
{
//TODO:Redirect to the users dashboard, when we have the routes.
console.log('EULA Accepted, Redirect somewhere else!');
}
});
},
function (error) {
localStorage.removeItem('loggedIn');
});
};
}]);
I have debugged the above and everything is working as it should, once FB is logged in, it queries the database, sets the user up with the data scraped from Facebook and then checks that data to see if the EULA has been accepted. If not then is should redirect to $location.path('/eula');
routes
.config(function($httpProvider, $stateProvider, $urlRouterProvider) {
$httpProvider.interceptors.push('httpRequestInterceptor');
$urlRouterProvider.otherwise('/');
$stateProvider
.state('login', {
url: '/',
templateUrl: 'templates/login.html',
data: {
requireLogin: false
}
})
.state('eula', {
url: '/eula',
templateUrl: 'templates/eula.html',
data: {
requireLogin: true
}
})
.state('accept', {
url: '/accept',
templateUrl: 'templates/accept.html',
data: {
requireLogin: true
}
})
});
as you are using ui-router (standard of ionic), you shall use states.
Replace : $location.path('/eula'); by $state.go('eula');
(and correspondign injected dependency)
Youre problem seems to have something to do with the execution.
OpenFB does not implement promises.
Look at these lines:
OpenFB.get('/me').success(function (user) {
localStorage.setItem('fbuser', JSON.stringify(user));
});
localStorage.setItem('provider', 'facebook');
It might take a while before OpenFB.get returns the result, but your code will execute the following instructions anyway, so you might find yourself in a situation where you're calling:
RequestService.get($scope.baseUrl + 'user')
before OpenFB.get('/me') has finished it's execution.
The best solution is to use ngOpenFB which implement promises: $q promises.
There's a sample here.
Your code should be implemented like this:
ngFB.login({scope: 'email, user_friends'})
.then(function(response){
// response.authResponse.accessToken
return ngFB.api({path: '/me'});
})
.then(function(user){
localStorage.setItem('fbuser', JSON.stringify(user));
localStorage.setItem('provider', 'facebook');
RequestService.get($scope.baseUrl + 'user')
.success(function(data, status, headers, config){
$scope.user = data;
//Set our logged in var
localStorage.setItem('loggedIn', true);
// Check if the user has accepted EULA
if($scope.user.eula == 0) {
return false;
} else
{
return true;
}
})
.error(function (data, status, headers, config) {
return false;
})
})
.then(function(eulaAccepted){
if (eulaAccepted)
{
$state.go('eula');
}
})
catch(function(reason){
localStorage.removeItem('loggedIn');
});
Each call returns a promise and they can be nested. The second .then( gets the result of the second promise (when it's been resolved) ngFB.api({path: '/me'}).
Since that call returns a promise with the object user, you'll be able to read it in the function.
This code:
if($scope.user.eula == 0) {
return false;
} else
{
return true;
}
does not return a promise but it's ok with this type of syntax.
In the third .then( :
.then(function(eulaAccepted){
if (eulaAccepted)
{
$state.go('eula');
}
})
you'll be able to read the value of the previous statement (eula).
Mind you, I haven't tested this code cause I don't have facebook, but the advantage of promises is to avoid indefinite levels of nested code
Ok so solved the issue, in my login check in the .run() method of my app I had a miss placed 'event.preventDefault();' call, once this was moved the works on both the desktop browser and on my device.
Related
I use CRA PWA and add toast that's show when onupdatefound and onstatechange. it's working well in android and pc but in iOS it's not shown and content not refresh till close tab and open it again.
I searching long time and not found any good answer for this issue.
I have ServiceWorkerWrapper.ts
import React, { FC, useEffect } from 'react';
import { Snackbar, Button } from '#material-ui/core';
import * as serviceWorker from '../serviceWorkerRegistration';
const ServiceWorkerWrapper: FC = () => {
const [showReload, setShowReload] = React.useState(false);
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
let timer:any;
if (counter >= 0 && showReload) {
setTimeout(() => setCounter(counter - 1), 1000)
} else if (showReload) {
reloadPage();
}
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [counter]);
const onSWUpdate = (registration: ServiceWorkerRegistration) => {
if (registration && registration.waiting) {
setShowReload(true);
setCounter(2)
} else {
setShowReload(false);
}
};
useEffect(() => {
serviceWorker.register({ onUpdate: onSWUpdate });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const reloadPage = () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg:any) => {
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
})
.catch((err) => console.log('Could not get registration: ', err));
}
};
return (
<Snackbar
open={showReload}
message="A new version is available!"
onClick={reloadPage}
ContentProps={{
className: "reload-snackbar"
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
action={
<Button
color="inherit"
size="small"
onClick={reloadPage}
>
Reload{showReload ? ` (${counter + 1})`:''}
</Button>
}
/>
);
}
export default ServiceWorkerWrapper;
serviceWorkerRegistration.ts
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
if (navigator.vendor === 'Apple Computer, Inc.') {
console.log('Safari!!!!');
if (registration.waiting) {
if (config && config.onUpdate) {
config.onUpdate(registration);
}
}
}
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}
service-worker.ts
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.
I am trying to call a Google cloud function from an Android app that does not work (First call just after deployment works 90 % of the times but subsequent calls fails, nothing is displayed on firebase log console either).
public Task<String> myCloudFunction() {
return FirebaseFunctions.getInstance()
.getHttpsCallable("createUser")
.call(data)
.continueWith(task -> {
String result = (String) task.getResult().getData();
return result;
});
}
Endpoint in Functions Dashboard
https://us-central1-xyz:555.cloudfunctions.net/createUser
This is how I call it.
public void callCloudFunction() {
createFirebaseUserAccount.myCloudFunction()
.addOnCompleteListener(new OnCompleteListener<String>() {
#Override
public void onComplete(#NonNull Task<String> task) {
if (!task.isSuccessful()) {
Exception e = task.getException();
if (e instanceof FirebaseFunctionsException) {
FirebaseFunctionsException ffe = (FirebaseFunctionsException) e;
FirebaseFunctionsException.Code code = ffe.getCode();
Object details = ffe.getDetails();
} else {
Timber.d(task.getResult());
}
}
}
});
}
Here is the cloud function:
$GOOGLE_APPLICATION_CREDENTIALS is pointing to service_key.json file which contains the private key.
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: "https://XYZ.firebaseio.com"
});
exports.createUser = functions.https.onCall((data, context) => {
const callerEmail = data.email;
const callerPassword = data.password;
const callerDisplayName = data.displayName;
return admin.auth().createUser({
email: callerEmail,
emailVerified: false,
password: callerPassword,
displayName: callerDisplayName,
disabled: false
}).then(userRecord => {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully created new user:", userRecord.uid);
return userRecord.uid;
}).catch(error => {
console.log("Error creating new user ", error);
return error;
});
});
Thanks for reading! :)
You're not returning a promise from the the function that contains the data to send to the client. Actually, you're not passing anything at all. You should instead return the promise chain from your async work:
return admin.auth().createUser({
email: callerEmail,
emailVerified: false,
password: callerPassword,
displayName: callerDisplayName,
disabled: false
}).then(userRecord => {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully created new user:", userRecord.uid);
return userRecord.uid;
}).catch(error => {
console.log("Error creating new user ", error);
return error;
});
Note the new return before the whole thing. You should definitely take some time to learn about how JavaScript promises work in order to make effective use of Cloud Functions, and they will not work correctly without observing their rules and conventions.
I ran into a bug whenever I run my React Native app on an Android device (physical and emulator). Yet, no problem at all on iOS. These functions are supposed to scan the database table for user handles and return an object if the handle already exists.
This is what the error looks like:
TypeError: Cannot read property 'handle' of null
at exports.handler (/var/task/index.js:7:36)
I'm using React Native, AWS Lambda, and EXPO.
This code lives within dbfunctions.js on the front end.
export async function scanHandles(){
return new Promise((resolve, reject) => {
let { auth } = store.getState()
let reqBody = {
userId: auth.user.username,
handle: auth.handle_update,
}
let path = '/u/scan-handle'
let myInit = {
headers: { 'Content-Type': 'application/json' },
body: reqBody,
}
console.log('myInit', myInit)
console.log('handle', auth.handle_update)
API.get(apiName, path, myInit)
.then((resp) => {
// if false, then handle does not exist
// if true, then handle already exists
resolve(resp)
})
.catch((error) => {
console.warn('Scan Handle', error)
reject(error)
})
})
}
Console logging auth.handle_update does print out the expected string. myInit also prints out the expected object.
On the back end, I'm using this for my scan:
const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient({ region: "us-west-1" });
exports.handler = (event, context, callback) => {
let e = JSON.parse(event.body);
var params = {
TableName: event.stageVariables.user,
FilterExpression: "handle = :handle",
ExpressionAttributeValues: { ":handle": e.handle }
};
docClient.scan(params, function(err, data) {
if (err) {
console.log("ERROR:", err);
let response = {
statusCode: err.statusCode,
headers: {},
body: JSON.stringify(err)
};
callback(response);
}
if (data.Count >= 1) {
// if user name exists
// call back handle exists response
let handleExistsResponse = {
statusCode: 200,
body: JSON.stringify({ Success: true })
};
callback(null, handleExistsResponse);
} else {
let response = {
statusCode: 200,
body: JSON.stringify({ Success: false })
};
callback(null, response);
}
});
};
Any idea as to why this would work on iOS and not Android?
EDIT:
Upon further testing, let e = JSON.parse(event.body) is returning null. So I console logged event and got a big ol object. Within this object, I found body and it's still null. So the body object isn't being passed it properly. Still confused about it working on iOS and not Android.
Did it!
Okay so API.get doesn't like body's being passed in. Instead, it wants a query parameter. So the lambda params looks like:
var params = {
TableName: event.stageVariables.user,
FilterExpression: "handle = :handle",
ExpressionAttributeValues: {
":handle": event["queryStringParameters"]["handle"]
}
};
And the front end function is:
export async function scanHandles(){
return new Promise((resolve, reject) => {
let { auth } = store.getState()
let handle = auth.handle_update
let path = `/u/scan-handle?handle=${handle}`
let myInit = {
headers: { 'Content-Type': 'application/json' },
}
API.get(apiName, path, myInit)
.then((resp) => {
// if false, then handle does not exist
// if true, then handle already exists
resolve(resp)
})
.catch((error) => {
console.warn('Scan Handle', error)
reject(error)
})
})
}
Works on both iOS and Android. Wonder why it wasn't working before?
I have a web app currently running with NodeJS and Express, where I authenticate the users using Passport sessions, and it works perfectly. Here is the overview of what I do:
app.use(session({
secret : 'hidden of course :)',
resave: false,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
/****** Passport functions ******/
passport.serializeUser(function (user, done) {
done(null, user.idUser);
});
passport.deserializeUser(function (id, done) {
db.user.findOne( { where : { idUser : id } }).then(function (user, err) {
done(null, user);
});
});
//Facebook
passport.use(new FacebookStrategy({
//Information stored on config/auth.js
clientID: *******,
clientSecret: ******,
callbackURL: *******,
profileFields: ['id', 'emails', 'displayName', 'name', 'gender', 'picture.type(large)']
}, function (accessToken, refreshToken, profile, done) {
//Using next tick to take advantage of async properties
process.nextTick(function () {
db.user.findOne( { where : { idUser : profile.id } }).then(function (user, err) {
if(err) {
return done(err);
}
if(user) {
return done(null, user);
} else {
// Check whether the email is undefined or valid
var emailTemp = '';
if(profile.emails && profile.emails[0] && profile.emails[0].value) {
emailTemp = profile.emails[0].value;
} else {
emailTemp = '';
}
var picture = '';
if(profile.photos && profile.photos[0] && profile.photos[0].value) {
picture = profile.photos[0].value;
} else {
picture = '/img/profile.png';
}
var sexFb = '';
if(profile.gender) {
sexFb = profile.gender;
} else {
sexFb = '';
}
// Create the user
db.user.create({
idUser : profile.id,
token : accessToken,
picture : picture,
nameUser : profile.displayName,
email : emailTemp,
sex : sexFb
}).then(function () {
db.user.findOne( { where : { idUser : profile.id } }).then(function (user, err) {
if(user) {
return done(null, user);
} else {
return done(err);
}
});
});
}
});
});
}));
app.use(express.static(__dirname + '/public/'));
/* FACEBOOK STRATEGY */
// Redirect the user to Facebook for authentication. When complete,
// Facebook will redirect the user back to the application at
// /auth/facebook/callback//
app.get('/auth/facebook', passport.authenticate('facebook', { scope : ['email']}));
/* FACEBOOK STRATEGY */
// Facebook will redirect the user to this URL after approval. Finish the
// authentication process by attempting to obtain an access token. If
// access was granted, the user will be logged in. Otherwise,
// authentication has failed.
app.get('/auth/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/' }),
function (req, res) {
// Successful authentication, redirect home.
res.redirect('../../app.html');
});
Now, I'm building our Android App and I need to authenticate our users, preferably using the backend I already built for the web version. I was checking some questions on SO like this one and I understood a lot of what I would have to do.
Currently my clients stay logged in through the cookie that Express-session handles, saving the user's id on the req.user, so that I can run queries like on this example:
app.put('/profile', function (req, res) {
//Updates the profile information of the user
db.user.update({
nameUser : req.body.nameUser
}, {
where : {
idUser : req.user.idUser
}
}).then(function (user) {
res.json({ yes : "yes" });
});
});
So my questions:
Can I authenticate my users using the same strategy as the one I currently have? If not, what would I have to change to be able to authenticate my users on Android?
Once they are authenticated, how can I set the req.user from Android (through HTTP request or whatever mean) to correctly perform the tasks on the backend (since all my requests are based on req.user)? I assume Android doesn't keep cookies like browsers do, so how would Passport Sessions know which user is calling the API?
I currently use Retrofit2 on my Android app to interact with the API, is it able to perform the actions necessary for this task?
Sorry for the long post, added a good chunk of code just to be on the safe side, if you guys have any questions or need any explanation please let me know!
I'm developing using IONIC framework. I'm having trouble with the hardware back button.
In android the hardware back button works perfectly, but the windows phone did not work out.
When I use the back button on windows phone minimizes the application and returns the device's home.
This function only works on android:
$ionicPlatform.registerBackButtonAction(function () {
console.log("Not work in WP");
}, 100);
help !!
I found solution.
In site
https://www.hoessl.eu/2014/12/on-using-the-ionic-framework-for-windows-phone-8-1-apps/
have a post calling -> Not fixed yet: Back Button
Not fixed yet: Back Button
With Windows Phone 8.0, listening on the “backbutton” event was pretty
simple, just as with android. On WP8.1, this event is not triggered
anymore. I haven’t figured out how to enable it yet. Any hint would be
appreciated.
But a user commented the solution. follows the passage that worked in
my case
Back Button Fix :
Set your $ionicPlatform.registerBackButtonAction
$ionicPlatform.registerBackButtonAction(function (evt) {
if (evt && evt.type == ‘backclick’) {
$ionicHistory.goBack();
return true;
}
}, 100);
Hookin WinJS and send it to Ionic :
if(ionic.Platform.isWindowsPhone)
{
WinJS.Application.onbackclick = function (evt) {
$ionicPlatform.hardwareBackButtonClick(evt);
return true;
}
}
Easy Fix, long time figuring it out
Example for placing the code inside app.js
angular.module('starter', ['ionic', 'starter.menu', 'starter.services'])
.run(function ($ionicPlatform, $ionicHistory, $state, ...) {
$ionicPlatform.registerBackButtonAction(function (evt) {
if (evt && evt.type == 'backclick') {
$ionicHistory.goBack();
return true;
}
}, 100);
...
$ionicPlatform.ready(function () {
...
if (ionic.Platform.isWindowsPhone()) {
WinJS.Application.onbackclick = function (evt) {
if ($state.current.name == 'app.home') {
//function responsible for exiting the application in Windows phone 8.1
cordova.exec(null, null, "ExitApp", "execute", []);
} else {
$ionicPlatform.hardwareBackButtonClick(evt);
return true;
}
}
}
}); ...
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'templates/login.html',
controller: 'LoginCtrl',
onEnter: function ($state, UserService) {
console.log("##### - " + UserService.get().isLogged);
if (UserService.get().isLogged) {
$state.go("app.home");
}
}
})
.state('app', {
url: '/app',
abstract: true,
templateUrl: 'templates/menu.html',
controller: 'MenuCtrl'
})
.state('app.secretary', {
url: '/secretary',
views: {
'menuContent': {
templateUrl: 'templates/secretary/menusecretary.html',
controller: 'MenuSecretaryCtrl'
}
}
})