import { __decorate, __awaiter, __rest, __param } from 'tslib';
import { Injectable, Injector, Optional, TemplateRef, ViewContainerRef, Directive, Input, ɵɵdefineInjectable, ɵɵinject, APP_INITIALIZER, NgModule } from '@angular/core';
import { first, map, tap, distinctUntilChanged, catchError, filter, take, switchMap, finalize } from 'rxjs/operators';
import { Storage } from '@bravobit/bb-foundation/storage';
import { Api, ApiError } from '@bravobit/bb-foundation/api';
import { BehaviorSubject, of, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { HttpErrorResponse, HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';

class AuthConfig {
}

class JwtHelper {
    constructor() {
        this.baseDecodeUnicode = (value) => {
            return decodeURIComponent(atob(value).replace(/(.)/g, (m, p) => {
                let code = p.charCodeAt(0).toString(16).toUpperCase();
                if (code.length < 2) {
                    code = '0' + code;
                }
                return '%' + code;
            }));
        };
        this.parse = (data) => {
            return {
                id: data['iss'] || null,
                type: data['typ'] || null,
                audience: data['aud'] || null,
                issuer: data['iss'] || null,
                subject: data['sub'] || null,
                role: data['role'] || null,
                notValidBefore: this.parseDate(data['nbf']),
                expiresAt: this.parseDate(data['exp']),
                issuedAt: this.parseDate(data['iat'])
            };
        };
        this.parseDate = (epochInSeconds) => {
            if (!epochInSeconds || epochInSeconds <= 0) {
                return null;
            }
            return new Date(epochInSeconds * 1000);
        };
    }
    decode(token) {
        try {
            const json = JSON.parse(this.urlDecode(token.split('.')[1]));
            return this.parse(json);
        }
        catch (_a) {
            return null;
        }
    }
    urlDecode(token) {
        const value = token || '';
        let output = value
            .replace(/-/g, '+')
            .replace(/_/g, '/');
        switch (output.length % 4) {
            case 0:
                break;
            case 2:
                output += '==';
                break;
            case 3:
                output += '=';
                break;
            default:
                throw 'Illegal base64url string!';
        }
        try {
            return this.baseDecodeUnicode(output);
        }
        catch (_a) {
            return atob(output);
        }
    }
}

let AuthMapper = class AuthMapper {
    constructor() {
        // Routes.
        this.me = 'auth/me';
        this.register = 'auth/register';
        this.resendCode = 'auth/resend';
        this.logout = 'auth/logout';
        this.refresh = 'auth/refresh';
        this.requestPassword = 'auth/reset';
        this.resetPassword = 'auth/reset-password';
    }
    role(data) {
        return (data && data.role) || null;
    }
    toRegister(data) {
        // Retrieve the params.
        const { token, refresh_token, user } = data;
        // Map the data to the correct format.
        return { accessToken: token, refreshToken: refresh_token, user: user };
    }
    toRefresh(data) {
        // Retrieve the params.
        const { token, refresh_token } = data;
        // Map the data to the correct format.
        return { accessToken: token, refreshToken: refresh_token };
    }
};
AuthMapper = __decorate([
    Injectable()
], AuthMapper);

class AuthVerifyProvider {
    constructor(_code, _verifyToken, _endpoint = 'auth/verify') {
        this._code = _code;
        this._verifyToken = _verifyToken;
        this._endpoint = _endpoint;
    }
    authenticate(api) {
        return __awaiter(this, void 0, void 0, function* () {
            // Execute API call.
            const { token, refresh_token, user } = yield api.post(this._endpoint, {
                token: this._code,
                verify_token: this._verifyToken
            });
            // Map the data to the correct format.
            return { accessToken: token, refreshToken: refresh_token, user: user };
        });
    }
}

class AuthEmailProvider {
    constructor(_email, _password, _endpoint = 'auth/login') {
        this._email = _email;
        this._password = _password;
        this._endpoint = _endpoint;
    }
    authenticate(api) {
        return __awaiter(this, void 0, void 0, function* () {
            // Execute API call.
            const { token, refresh_token, user, provider, verify_token } = yield api.post(this._endpoint, {
                email: this._email,
                password: this._password
            });
            // Map the data to the correct format.
            return {
                accessToken: token,
                refreshToken: refresh_token,
                user: user,
                provider: provider,
                verifyToken: verify_token
            };
        });
    }
}

let Auth = class Auth {
    constructor(_storage, _mapper, _injector, _api, _config) {
        this._storage = _storage;
        this._mapper = _mapper;
        this._injector = _injector;
        this._api = _api;
        this._config = _config;
        // Data.
        this._user$ = new BehaviorSubject(null);
        // Validate the API was injected.
        if (!this._api) {
            throw new Error('The API is required for this module to work.');
        }
        // Set the storage keys based on the application id.
        const applicationId = this.getConfigString('applicationId') || 'angular';
        this._apiUserStorageKey = `${applicationId}_auth-user`;
        this._apiTokenStorageKey = `${applicationId}_auth-token`;
        this._apiRefreshTokenStorageKey = `${applicationId}_auth-refresh-token`;
        // Fetch the user from the storage.
        const user = this._storage.cookie.get(this._apiUserStorageKey);
        // Cache the user in memory.
        this._user$.next(user);
    }
    initialize() {
        return () => __awaiter(this, void 0, void 0, function* () {
            const shouldBootstrap = this.getConfigBoolean('bootstrap', true);
            // Check if the app should bootstrap
            // the authentication.
            if (!shouldBootstrap) {
                return;
            }
            // Only retrieve from the server
            // when an access token is saved.
            if (!this.accessToken && !this.refreshToken) {
                return;
            }
            // Try to fetch the user from the server.
            const user = yield this.me().pipe(first()).toPromise();
            // Save the user in the storage.
            return this.setUser(user);
        });
    }
    get isAuthenticated() {
        return !!this.payload;
    }
    get user() {
        return this._user$.asObservable();
    }
    get accessToken() {
        return this._storage.cookie.get(this._apiTokenStorageKey);
    }
    get refreshToken() {
        return this._storage.cookie.get(this._apiRefreshTokenStorageKey);
    }
    get payload() {
        return new JwtHelper().decode(this.accessToken);
    }
    me() {
        return this._api.get(this._mapper.me);
    }
    signIn(provider, as) {
        return __awaiter(this, void 0, void 0, function* () {
            const _a = yield provider.authenticate(this._api), { accessToken, refreshToken, user } = _a, result = __rest(_a, ["accessToken", "refreshToken", "user"]);
            // Check if the role matches.
            if (as && !as.includes(this._mapper.role(user))) {
                throw new Error('Invalid role.');
            }
            // Validate if the provider is one of the available
            // providers then return the user object and the provider.
            const apiProvider = (result && result.provider) || null;
            const apiVerifyToken = (result && result.verifyToken) || null;
            const availableProviders = ['email', 'sms', 'totp'];
            if (availableProviders.includes(apiProvider)) {
                return { user, provider: apiProvider, verifyToken: apiVerifyToken };
            }
            // Set the tokens in storage.
            this.setTokens(accessToken, refreshToken);
            // Set the user in storage.
            this.setUser(user);
            // Return the user.
            return { user };
        });
    }
    signInWithEmail(email, password, as) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.signIn(new AuthEmailProvider(email, password), as);
        });
    }
    signInWithVerifyCode(code, verifyToken) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.signIn(new AuthVerifyProvider(code, verifyToken));
        });
    }
    resendVerifyCode(verifyToken) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this._api.post(this._mapper.resendCode, {
                verify_token: verifyToken
            });
        });
    }
    register(data) {
        return __awaiter(this, void 0, void 0, function* () {
            // Execute API call.
            const result = yield this._api.post(this._mapper.register, data);
            // Map to the correct response.
            const { accessToken, refreshToken, user } = this._mapper.toRegister(result);
            // Set the tokens in storage.
            this.setTokens(accessToken, refreshToken);
            // Set the user in storage.
            this.setUser(user);
            // Return the user.
            return user;
        });
    }
    logout() {
        try {
            // Log the user out.
            this._api.get(this._mapper.logout, { headers: { Authorization: this.refreshToken } })
                .toPromise().then(_ => _);
        }
        catch (_a) {
            // Do nothing because the tokens will be deleted anyways from the storage.
        }
        // Delete the tokens from the session.
        return this.clear();
    }
    refresh() {
        // If the refresh token does
        // not exist just return an observable of null.
        if (!this.refreshToken) {
            return of(null);
        }
        // Perform the refresh call.
        const scheme = this.getConfigString('schema', 'Bearer');
        return this._api.get(this._mapper.refresh, {
            headers: {
                'Authorization': `${scheme} ${this.refreshToken}`,
                'X-No-Authorization': 'yes'
            }
        }).pipe(map(data => this._mapper.toRefresh(data)), tap(({ accessToken, refreshToken }) => this.setTokens(accessToken, refreshToken)), map(({ accessToken }) => accessToken));
    }
    requestPassword(email) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return yield this._api.post(this._mapper.requestPassword, { email });
            }
            catch (error) {
                throw error;
            }
        });
    }
    resetPassword(token, newPassword) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return yield this._api.post(this._mapper.resetPassword, { token, password: newPassword });
            }
            catch (error) {
                throw error;
            }
        });
    }
    guard(type, redirectUrl) {
        let newUrl = null;
        // Get the correct new url.
        switch (type) {
            case 'authenticated':
                newUrl = this.getConfigString('loggedInUrl', null);
                break;
            case 'unauthenticated':
                newUrl = this.getConfigString('redirectUrl', null);
                break;
        }
        // Append a redirect url if the user is deemed
        // unauthenticated.
        if (type === 'unauthenticated') {
            const setRedirectOnFailedAuth = this.getConfigBoolean('setRedirectOnFailedAuth', true);
            if (setRedirectOnFailedAuth && redirectUrl && newUrl) {
                newUrl += `?redirectUrl=${redirectUrl}`;
            }
        }
        // Parse the url if it exists.
        if (this.router && newUrl) {
            return this.router.parseUrl(newUrl);
        }
        // Return false if the user is not allowed.
        return false;
    }
    setTokens(accessToken, refreshToken) {
        // Set the token if the token exists.
        if (accessToken) {
            const { expiresAt } = new JwtHelper().decode(accessToken);
            this._storage.cookie.set(this._apiTokenStorageKey, accessToken, {
                expires: expiresAt
            });
        }
        // Set the refresh token if the refresh token exists.
        if (refreshToken) {
            const { expiresAt } = new JwtHelper().decode(refreshToken);
            this._storage.cookie.set(this._apiRefreshTokenStorageKey, refreshToken, {
                expires: expiresAt
            });
        }
    }
    setUser(user) {
        // Validate the user exists.
        if (!user) {
            return;
        }
        // Push the next user.
        this._user$.next(user);
        // Make the user data valid for one year.
        const date = new Date();
        date.setFullYear(date.getFullYear() + 1);
        const timestamp = date.getTime();
        // Save the user in the storage.
        return this._storage.cookie.set(this._apiUserStorageKey, user, {
            expires: timestamp
        });
    }
    clear() {
        // Delete the items from the local storage.
        this._storage.cookie.remove(this._apiUserStorageKey);
        this._storage.cookie.remove(this._apiTokenStorageKey);
        this._storage.cookie.remove(this._apiRefreshTokenStorageKey);
        // Push an empty user.
        this._user$.next(null);
    }
    get router() {
        return this._injector.get(Router);
    }
    getConfigBoolean(name, defaultValue = false) {
        return (this._config && this._config[name]) === undefined
            ? defaultValue
            : this._config[name];
    }
    getConfigString(name, defaultValue = null) {
        return (this._config && this._config[name]) === undefined
            ? defaultValue
            : this._config[name];
    }
};
Auth.ctorParameters = () => [
    { type: Storage },
    { type: AuthMapper },
    { type: Injector },
    { type: Api, decorators: [{ type: Optional }] },
    { type: AuthConfig, decorators: [{ type: Optional }] }
];
Auth = __decorate([
    Injectable(),
    __param(3, Optional()),
    __param(4, Optional())
], Auth);

let BbAuthenticated = class BbAuthenticated {
    constructor(_auth, _template, _viewContainerRef) {
        this._auth = _auth;
        this._template = _template;
        this._viewContainerRef = _viewContainerRef;
    }
    ngOnInit() {
        this._subscription = this._auth.user.pipe(map(user => !!user), distinctUntilChanged()).subscribe(isAuthenticated => {
            if (isAuthenticated) {
                this._viewContainerRef.createEmbeddedView(this._template);
            }
            else {
                this._viewContainerRef.clear();
            }
        });
    }
    ngOnDestroy() {
        this._subscription && this._subscription.unsubscribe();
    }
};
BbAuthenticated.ctorParameters = () => [
    { type: Auth },
    { type: TemplateRef },
    { type: ViewContainerRef }
];
BbAuthenticated = __decorate([
    Directive({
        selector: '[bbAuthenticated]'
    })
], BbAuthenticated);

let BbRole = class BbRole {
    constructor(_auth, _template, _viewContainerRef) {
        this._auth = _auth;
        this._template = _template;
        this._viewContainerRef = _viewContainerRef;
        this.getPermissions = (value) => {
            return Array.isArray(value) ? value : [value];
        };
    }
    ngOnInit() {
        this._subscription = this._auth.user.pipe(map(user => (user && user.role) || null), distinctUntilChanged()).subscribe(role => {
            const permissions = this.getPermissions(this.bbRole);
            if (permissions.includes(role)) {
                this._viewContainerRef.createEmbeddedView(this._template);
            }
            else {
                this._viewContainerRef.clear();
            }
        });
    }
    ngOnDestroy() {
        this._subscription && this._subscription.unsubscribe();
    }
};
BbRole.ctorParameters = () => [
    { type: Auth },
    { type: TemplateRef },
    { type: ViewContainerRef }
];
__decorate([
    Input()
], BbRole.prototype, "bbRole", void 0);
BbRole = __decorate([
    Directive({
        selector: '[bbRole]'
    })
], BbRole);

let BbAnonymousGuard = class BbAnonymousGuard {
    constructor(_auth) {
        this._auth = _auth;
    }
    canActivate(route, state) {
        return __awaiter(this, void 0, void 0, function* () {
            // Check if the user is authenticated.
            const isAuthenticated = yield this._auth.user.pipe(map(user => !!user), first()).toPromise();
            // If the user is not authenticated it can
            // access the anonymous page.
            if (!isAuthenticated) {
                return true;
            }
            // Return the user back to the authenticated page.
            return this._auth.guard('authenticated', state.url);
        });
    }
    canActivateChild(childRoute, state) {
        return this.canActivate(childRoute, state);
    }
};
BbAnonymousGuard.ctorParameters = () => [
    { type: Auth }
];
BbAnonymousGuard.ɵprov = ɵɵdefineInjectable({ factory: function BbAnonymousGuard_Factory() { return new BbAnonymousGuard(ɵɵinject(Auth)); }, token: BbAnonymousGuard, providedIn: "root" });
BbAnonymousGuard = __decorate([
    Injectable({
        providedIn: 'root'
    })
], BbAnonymousGuard);

let BbAuthenticatedGuard = class BbAuthenticatedGuard {
    constructor(_auth) {
        this._auth = _auth;
    }
    canActivate(route, state) {
        return __awaiter(this, void 0, void 0, function* () {
            // Check if the user is authenticated.
            const isAuthenticated = yield this._auth.user.pipe(map(user => !!user), first()).toPromise();
            // If the user is authenticated it can
            // access the authenticated page.
            if (isAuthenticated) {
                return true;
            }
            // The user is not authorized to see that
            // page so un-authorize him.
            return this._auth.guard('unauthenticated', state.url);
        });
    }
    canActivateChild(childRoute, state) {
        return this.canActivate(childRoute, state);
    }
};
BbAuthenticatedGuard.ctorParameters = () => [
    { type: Auth }
];
BbAuthenticatedGuard.ɵprov = ɵɵdefineInjectable({ factory: function BbAuthenticatedGuard_Factory() { return new BbAuthenticatedGuard(ɵɵinject(Auth)); }, token: BbAuthenticatedGuard, providedIn: "root" });
BbAuthenticatedGuard = __decorate([
    Injectable({
        providedIn: 'root'
    })
], BbAuthenticatedGuard);

let AuthInterceptor = class AuthInterceptor {
    constructor(_auth, _router, _config) {
        this._auth = _auth;
        this._router = _router;
        this._config = _config;
        // Data.
        this.isRefreshing = false;
        this.token$ = new BehaviorSubject(null);
        this.getToken = (request) => {
            // Get the token based on header.
            if (request.headers.has('Authorization')) {
                return request.headers.get('Authorization');
            }
            // Return the default access token.
            return this._auth.accessToken;
        };
        this.composeRequest = (request, token = null) => {
            // If the token is not set, delete the authorization header.
            if (!token) {
                return request.clone({ headers: request.headers.delete('Authorization') });
            }
            // Get the auth scheme from the config,
            // if the scheme was not set use bearer.
            const authScheme = (this._config && this._config.scheme) || 'Bearer';
            // Clone and add the authorization header.
            return request.clone({
                setHeaders: { Authorization: `${authScheme} ${token}` }
            });
        };
    }
    intercept(request, next) {
        // Just return when the user is performing an refresh request.
        if (request.headers.has('X-No-Authorization')) {
            const newRequest = request.clone({ headers: request.headers.delete('X-No-Authorization') });
            return next.handle(newRequest);
        }
        // Compose the new request.
        const newRequest = this.composeRequest(request, this.getToken(request));
        // Handle all errors.
        return next.handle(newRequest).pipe(catchError(error => {
            // Create a custom error.
            const apiError = new ApiError(error.status, error.error);
            // Check if it is an error response.
            if (!(error instanceof HttpErrorResponse)) {
                return throwError(apiError);
            }
            // Handle a 401 error.
            if (error.status === 401) {
                return this.handle401Error(request, next);
            }
            // Just re-throw the error.
            return throwError(apiError);
        }));
    }
    handle401Error(request, next) {
        // If already refreshing wait for the token to complete.
        if (this.isRefreshing) {
            return this.token$.pipe(filter(token => token !== null), take(1), switchMap(token => next.handle(this.composeRequest(request, token))));
        }
        // Set the refreshing to true.
        this.isRefreshing = true;
        this.token$.next(null);
        return this._auth.refresh().pipe(switchMap(newToken => {
            if (!newToken) {
                return throwError('No refresh token was available.');
            }
            this.token$.next(newToken);
            return next.handle(this.composeRequest(request, newToken));
        }), catchError(error => {
            return this.logoutUser(error);
        }), finalize(() => {
            this.isRefreshing = false;
        }));
    }
    logoutUser(error) {
        // Delete the tokens from the session.
        this._auth.clear();
        // Compose the route url.
        const routeUrl = this._config && this._config.redirectUrl;
        // Route back if the user provided a redirect url.
        if (this._router && routeUrl) {
            this._router.navigate([routeUrl]).then(_ => _);
        }
        // Throw the error.
        return throwError(error || 'Invalid refresh token; user was logged out.');
    }
};
AuthInterceptor.ctorParameters = () => [
    { type: Auth },
    { type: Router, decorators: [{ type: Optional }] },
    { type: AuthConfig, decorators: [{ type: Optional }] }
];
AuthInterceptor = __decorate([
    Injectable(),
    __param(1, Optional()),
    __param(2, Optional())
], AuthInterceptor);

var AuthModule_1;
const DECLARATIONS_EXPORTS = [
    BbAuthenticated,
    BbRole
];
let AuthModule = AuthModule_1 = class AuthModule {
    static forRoot(config) {
        return {
            ngModule: AuthModule_1,
            providers: [
                Auth,
                { provide: AuthConfig, useValue: config },
                { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
                { useFactory: initializeAuth, provide: APP_INITIALIZER, deps: [Auth], multi: true }
            ]
        };
    }
};
AuthModule = AuthModule_1 = __decorate([
    NgModule({
        imports: [HttpClientModule],
        declarations: [...DECLARATIONS_EXPORTS],
        exports: [...DECLARATIONS_EXPORTS],
        providers: [
            { provide: AuthMapper, useClass: AuthMapper }
        ]
    })
], AuthModule);
function initializeAuth(auth) {
    return auth.initialize();
}

/**
 * Generated bundle index. Do not edit.
 */

export { Auth, AuthConfig, AuthEmailProvider, AuthMapper, AuthModule, AuthVerifyProvider, BbAnonymousGuard, BbAuthenticated, BbAuthenticatedGuard, BbRole, JwtHelper, initializeAuth, AuthInterceptor as ɵa };
//# sourceMappingURL=bravobit-bb-foundation-auth.js.map
