Весной нам в голову пришла идея сделать простой сервис для облачного бэкапа серверов. Поскольку в то время работа над проектом велась преимущественно по вечерам и по выходным, для ускорения процесса было решено использовать только те технологии, в которых у нас есть опыт. Для backend-части был выбран Django, а реализация клиентской части предполагалась в виде SPA на базе AngularJS. Задумка была в следующем: сделать продукт с минимальным функционалом, а затем постепенно добавлять новые возможности. Для этого необходимо было сделать достаточно гибкую и масштабируемую систему. Немного пораскинув мозгами, мы приступили.
Роутинги
И первый вопрос, который возник, был связан с роутингами в клиентской части. Нам была необходима надёжная и простая система, которая поддерживала бы вложенные друг в друга шаблоны и позволяла однозначно сопоставлять определённому URL необходимый шаблон. После недолгих поисков мы выбрали ui-router.
Была утверждена следующая схема:
По пути /
пользователю показывается лэндинг, который никак не связан с приложением. При переходе на /app/
сервер отдаёт файл app.html
, который содержит весь head, все скрипты в конце body и один единственный div
со скромным атрибутом ui-view
. Именно в этот div
грузится всё приложение. В зависимости от того, залогинен пользователь или нет, ему показываются разные заполнения этого div
’a.
Я не буду забегать вперёд, а рассмотрю случай для аутентифицированного пользователя. Итак, в этом случае, если в URL после /app/
нет никакого хэша, то внутрь <div ui-view></div>
грузится следуюший слой: index.html
. Этот файл содержит в себе статическую часть приложения, которая окружает всю рабочую область: хэдер, футер и боковое меню. В index.html
так же есть div
с атрибутом ui-view
, в который будет подгружаться ещё один уровень приложения, а конкретно — различные экраны (в нашем случае это: главный экран, детальный экран сервера, экран биллинга, экран восстановления бэкапа и другие).
Рассмотрим, как же это всё описано с помощью ui-router:
app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('index', {
url: '/',
templateUrl: '/static/views/index.html'
})
.state('index.main', {
url: '^/main',
templateUrl: '/static/views/pages/main.html'
})
.state('index.client', {
url: '^/main/c/:id',
templateUrl: '/static/views/pages/client.html'
})
.state('index.billing', {
url: '^/billing',
templateUrl: '/static/views/pages/billing.html'
})
.state('index.restore', {
url: '^/restore',
templateUrl: '/static/views/pages/restore.html'
});
$urlRouterProvider.otherwise('/main'); // Если хэш не совпадает ни с одним, то редирект на страницу /main
}])
Публичные и приватные страницы
Настало время задуматься над разграничением прав доступа пользователей к определённым страницам. Если пользователь не залогинен, то ему могут показываться только публичные страницы, а при попытке захода на приватную страницу его ждёт принудительный редирект на экран логина. Так же и в обратную сторону: если пользователь уже вошёл, то он не сможет увидеть страницы входа, регистрации и восстановления пароля.
Итак, добавим данные о публичных страницах в конфигурацию роутера:
$stateProvider
.state('login', {
url: '/login',
templateUrl: '/static/views/login.html'
})
.state('signup', {
url: '/signup',
templateUrl: '/static/views/signup.html'
})
.state('recovery', {
url: '/recovery',
templateUrl: '/static/views/recovery.html'
});
В модуле, отвечающем за авторизацию, создана фабрика, которая определяет залогинен ли пользователь:
AuthModule.factory('Auth', ['$cookieStore', function ($cookieStore) {
var currentUser = $cookieStore.get('login') || 0,
publicStates = ['login', 'signup', 'recovery'];
return {
authorize: function(state) {
return (this.isLoggedIn() && (publicStates.indexOf(state) < 0)) || (!this.isLoggedIn() && (publicStates.indexOf(state) >= 0))
},
isLoggedIn: function() {
return !!currentUser;
}
}
}])
Метод isLoggedIn
возвращает true
, если пользователь залогинен, либо false
в противном случае. Метод authorize
определяет для текущего состояния, имеет ли право пользователь в нём находиться.
Использование этих методов осуществляется в обработчике события $stateChangeStart
, которое возникает в момент начала изменения состояния:
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
// Если пользователь не имеет права находиться в данном состоянии
if (!Auth.authorize(toState.name)) {
// Необходимо для предотвращения дальнейшего изменения состояния
event.preventDefault();
// Для случая первичного определения пути (при заходе на /app/ без какого-либо хэша)
if (fromState.url === '^') {
if (Auth.isLoggedIn()) {
$state.go('index.main');
} else {
$state.go('auth');
}
}
}
});
Аутентификация
Процедура аутентификации на стороне клиента реализована с помощью функции в фабрике Auth
:
login: function (user, success, error) {
$http.post('/login/', user)
.success(function () {
currentUser = 1;
success();
})
.error(error);
}
Вызов этой функции производится в контроллере. В качестве аргументов передаются username
, password
и коллбэки:
Auth.login({
username: $scope.login.username,
password: $scope.login.password
},
function () {
$state.go('index.main');
},
function () {
$scope.login.error = true;
});
На сервере с помощью стандартных django-сессий хранится информация о пользователе (его id). Для этого используются стандартные методы django.contrib.auth
.
from django.contrib.auth import authenticate, login
def login_service(request):
data = json.loads(request.body)
user = authenticate(username=data['username'], password=data['password'])
if user is not None:
login(request, user)
return HttpResponse(status=200)
else:
return HttpResponse('Login error', status=401)
Во время каждого http-запроса сервер проверяет, залогинен ли пользователь, и устанавливает в заголовок 'Set-Cookie' соответствующее значение. Это значение и проверяется в клиентской части с помощью $cookieStore.get('login')
.
Связка между моделями сервера и клиента
С целью ускорения разработки и повышения гибкости приложения, было решено использовать middleware между Django и AngularJS. Выбор пал на django-angular.
Основные его преимущества:
- предоставляет возможность выполнять основные CRUD операции;
- позволяет плотно связать django-формы и angular-контроллеры;
- даёт функционал для вызова методов в django прямо из angular-контроллера.
Подробнее об установке и настройке можно прочитать в документации.
Итог
В итоге у нас получилось расширяемое и гибкое приложение, в котором модули минимально зависят друг от друга. Код всех модулей хранится в отдельных файлах, а добавление новых оставляет другие нетронутыми. Кроме того, функционал django-angular значительно ускоряет разработку.
Эта вторая статья из цикла про то, как мы делали сервис облачного резервного копирования серверов bitcalm.com.
Первая статья: Разработка своей системы биллинга на Django
Автор: m_smirnov