Статьи

Как мы создали прикладную среду, используя AngularJS и Django

Весной у нас появилась идея сделать  простой сервис облачного резервного копирования для серверов Linux . С тех пор работа над проектом велась в основном по вечерам и в выходные дни, для ускорения процесса было решено использовать только те технологии, в которых мы имеем опыт. Для серверной части был выбран Django, а реализация клиента часто предполагалась как SPA на основе AngularJS. Идея: сделать продукт с минимальными функциональными возможностями, а затем постепенно добавлять новые функции. Необходимо было сделать достаточно гибкую и масштабируемую систему. 

Маршрутизация

И первый возникший вопрос был связан с маршрутизацией на стороне клиента. Нам нужна была надежная и простая система, которая бы поддерживала вложенные друг в друге шаблоны и однозначно соотносила желаемый шаблон URL. После недолгого поиска мы выбрали  ui-router .

Была утверждена следующая схема:

Кстати / пользователю показывается лендинг, который не имеет никакого отношения к приложению. При переключении в / app / file server выдает app.html, который содержит всю заголовок, все скрипты в конце одного тела и div с скромным атрибутом ui-view. Именно в этот div загружено все приложение. В зависимости от того, залогинен ли пользователь или нет, он показывает различное наполнение этого div’а.

Я не буду забегать вперед и рассмотрим случай для аутентифицированного пользователя. Итак, в этом случае в URL после хеша / app / no внутри <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');  // If the hash does not match one, then redirect to the page / 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;
        }
    }

}])

Метод Is Logged In возвращает true, если пользователь вошел в систему, или false в противном случае. Метод определяет полномочия для текущего состояния, имеет право на пользователя быть в нем.

Использование этих методов выполняется в обработчике событий $ stateChangeStart, который происходит в начале изменений состояния:

$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
    // If the user has no right to be in this state
    if (!Auth.authorize(toState.name)) {
        // It is necessary to prevent further changes in the state
        event.preventDefault();
        // For the case of the primary ways of determining (when entering the / app / without hash)        
        if (fromState.url === '^') {
            if (Auth.isLoggedIn()) {
                $state.go('index.main');
            } else {
                $state.go('auth');
            }
        }
    }
});

Аутентификация

Процедура аутентификации на стороне клиента реализована с использованием заводской аутентификации:

login: function (user, success, error) {
    $http.post('/login/', user)
        .success(function () {
            currentUser = 1;
            success();
        })
        .error(error);
}

Вызов этой функции осуществляется на контроллере. В аргументы передаются имя пользователя, пароль и обратные вызовы:

Auth.login({
    username: $scope.login.username,
    password: $scope.login.password
},
function () {
    $state.go('index.main');
},
function () {
    $scope.login.error = true;
});

На сервере со стандартными django-сессиями хранится пользовательская информация (ее идентификатор). Он использует стандартные методы 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’).

Связь между моделью сервера и клиента

Чтобы ускорить разработку и повысить гибкость приложения, было решено использовать промежуточное программное обеспечение между Django и AngularJS. Выбор пал на  джанго-угловой .

Его основные преимущества:

  • предоставляет возможность выполнять основные   операции CRUD ;
  • позволяет жестко завязывать джанго-формы и угловые контроллеры;
  • обеспечивает функциональность для вызова методов в Django прямо из Angular-контроллера.

Более подробную информацию об установке и настройке можно найти в  документации .