pixelfed/pixelfed

View on GitHub
resources/assets/components/invite/AdminInvite.vue

Summary

Maintainability
Test Coverage
<template>
    <div class="admin-invite-component">
        <div class="admin-invite-component-inner">
            <div class="card bg-dark">
                <div v-if="tabIndex === 0" class="card-body d-flex align-items-center justify-content-center">
                    <div class="text-center">
                        <b-spinner variant="muted" />
                        <p class="text-muted mb-0">Loading...</p>
                    </div>
                </div>

                <div v-else-if="tabIndex === 1" class="card-body">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                        <p class="mb-0 text-muted">
                            <span>{{ instance.stats.user_count.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"}) }} users</span>
                            <span>ยท</span>
                            <span>{{ instance.stats.status_count.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"}) }} posts</span>
                        </p>

                        <div v-if="inviteConfig.message != 'You\'ve been invited to join'">
                            <div class="admin-message">
                                <p class="small text-light mb-0">Message from admin(s):</p>
                                {{ inviteConfig.message }}
                            </div>
                        </div>
                    </div>

                    <div class="mt-5">
                        <div class="form-group">
                            <label for="username">Username</label>
                            <input
                                type="text"
                                class="form-control form-control-lg"
                                placeholder="What should everyone call you?"
                                minlength="2"
                                maxlength="15"
                                v-model="form.username" />

                            <p v-if="errors.username" class="form-text text-danger">
                                <i class="far fa-exclamation-triangle mr-1"></i>
                                {{ errors.username }}
                            </p>
                        </div>

                        <button
                            class="btn btn-primary btn-block font-weight-bold"
                            @click="proceed(tabIndex)"
                            :disabled="isProceeding || !form.username || form.username.length < 2">
                            <template v-if="isProceeding">
                                <b-spinner small />
                            </template>
                            <template v-else>
                                Continue
                            </template>
                        </button>

                        <p class="login-link">
                            <a href="/login">Already have an account?</a>
                        </p>

                        <p class="register-terms">
                            By registering, you agree to our <a href="/site/terms">Terms of Service</a> and <a href="/site/privacy">Privacy Policy</a>.
                        </p>
                    </div>
                </div>

                <div v-else-if="tabIndex === 2" class="card-body">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                    </div>
                    <div class="mt-5">
                        <div class="form-group">
                            <label for="username">Email Address</label>
                            <input
                                type="email"
                                class="form-control form-control-lg"
                                placeholder="Your email address"
                                v-model="form.email" />

                            <p v-if="errors.email" class="form-text text-danger">
                                <i class="far fa-exclamation-triangle mr-1"></i>
                                {{ errors.email }}
                            </p>
                        </div>

                        <button
                            class="btn btn-primary btn-block font-weight-bold"
                            @click="proceed(tabIndex)"
                            :disabled="isProceeding || !form.email || !validateEmail()">
                            <template v-if="isProceeding">
                                <b-spinner small />
                            </template>
                            <template v-else>
                                Continue
                            </template>
                        </button>
                    </div>
                </div>

                <div v-else-if="tabIndex === 3" class="card-body">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                    </div>
                    <div class="mt-5">
                        <div class="form-group">
                            <label for="username">Password</label>
                            <input
                                type="password"
                                class="form-control form-control-lg"
                                placeholder="Use a secure password"
                                minlength="8"
                                v-model="form.password" />

                            <p v-if="errors.password" class="form-text text-danger">
                                <i class="far fa-exclamation-triangle mr-1"></i>
                                {{ errors.password }}
                            </p>
                        </div>

                        <button
                            class="btn btn-primary btn-block font-weight-bold"
                            @click="proceed(tabIndex)"
                            :disabled="isProceeding || !form.password || form.password.length < 8">
                            <template v-if="isProceeding">
                                <b-spinner small />
                            </template>
                            <template v-else>
                                Continue
                            </template>
                        </button>
                    </div>
                </div>

                <div v-else-if="tabIndex === 4" class="card-body">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                    </div>
                    <div class="mt-5">
                        <div class="form-group">
                            <label for="username">Confirm Password</label>
                            <input
                                type="password"
                                class="form-control form-control-lg"
                                placeholder="Use a secure password"
                                minlength="8"
                                v-model="form.password_confirm" />

                            <p v-if="errors.password_confirm" class="form-text text-danger">
                                <i class="far fa-exclamation-triangle mr-1"></i>
                                {{ errors.password_confirm }}
                            </p>
                        </div>

                        <button
                            class="btn btn-primary btn-block font-weight-bold"
                            @click="proceed(tabIndex)"
                            :disabled="isProceeding || !form.password_confirm || form.password !== form.password_confirm">
                            <template v-if="isProceeding">
                                <b-spinner small />
                            </template>
                            <template v-else>
                                Continue
                            </template>
                        </button>
                    </div>
                </div>

                <div v-else-if="tabIndex === 5" class="card-body">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                    </div>
                    <div class="mt-5">
                        <div class="form-group">
                            <label for="username">Display Name</label>
                            <input
                                type="text"
                                class="form-control form-control-lg"
                                placeholder="Add an optional display name"
                                minlength="8"
                                v-model="form.display_name" />

                            <p v-if="errors.display_name" class="form-text text-danger">
                                <i class="far fa-exclamation-triangle mr-1"></i>
                                {{ errors.display_name }}
                            </p>
                        </div>

                        <button
                            class="btn btn-primary btn-block font-weight-bold"
                            @click="proceed(tabIndex)"
                            :disabled="isProceeding">
                            <template v-if="isProceeding">
                                <b-spinner small />
                            </template>
                            <template v-else>
                                Continue
                            </template>
                        </button>
                    </div>
                </div>

                <div v-else-if="tabIndex === 6" class="card-body d-flex flex-column">
                    <div class="d-flex justify-content-center my-3">
                        <img src="/img/pixelfed-icon-color.png" width="60" alt="Pixelfed logo" />
                    </div>
                    <div class="d-flex flex-column align-items-center justify-content-center">
                        <p class="lead mb-1 text-muted">You've been invited to join</p>
                        <p class="h3 mb-2">{{ instance.uri }}</p>
                    </div>
                    <div class="mt-5 d-flex align-items-center justify-content-center flex-column flex-grow-1">
                        <b-spinner variant="muted" />
                        <p class="text-muted">Registering...</p>
                    </div>
                </div>

                <div v-else-if="tabIndex === 'invalid-code'" class="card-body d-flex align-items-center justify-content-center">
                    <div>
                        <h1 class="text-center">Invalid Invite Code</h1>
                        <hr>
                        <p class="text-muted mb-1">The invite code you were provided is not valid, this can happen when:</p>
                        <ul class="text-muted">
                            <li>Invite code has typos</li>
                            <li>Invite code was already used</li>
                            <li>Invite code has reached max uses</li>
                            <li>Invite code has expired</li>
                            <li>You have been rate limited</li>
                        </ul>
                        <hr>
                        <a href="/" class="btn btn-primary btn-block rounded-pill font-weight-bold">Go back home</a>
                    </div>
                </div>

                <div v-else class="card-body">
                    <p>An error occured.</p>
                </div>
            </div>
        </div>
    </div>
</template>

<script type="text/javascript">
    export default {
        props: ['code'],

        data() {
            return {
                instance: {},
                inviteConfig: {},
                tabIndex: 0,
                isProceeding: false,
                errors: {
                    username: undefined,
                    email: undefined,
                    password: undefined,
                    password_confirm: undefined
                },

                form: {
                    username: undefined,
                    email: undefined,
                    password: undefined,
                    password_confirm: undefined,
                    display_name: undefined,
                }
            }
        },

        mounted() {
            this.fetchInstanceData();
        },

        methods: {
            fetchInstanceData() {
                axios.get('/api/v1/instance')
                .then(res => {
                    this.instance = res.data;
                })
                .then(res => {
                    this.verifyToken();
                })
                .catch(err => {
                    console.log(err);
                })
            },

            verifyToken() {
                axios.post('/api/v1.1/auth/invite/admin/verify', {
                    token: this.code,
                })
                .then(res => {
                    this.tabIndex = 1;
                    this.inviteConfig = res.data;
                })
                .catch(err => {
                    this.tabIndex = 'invalid-code';
                })
            },

            checkUsernameAvailability() {
                axios.post('/api/v1.1/auth/invite/admin/uc', {
                    token: this.code,
                    username: this.form.username
                })
                .then(res => {
                    if(res && res.data) {
                        this.isProceeding = false;
                        this.tabIndex = 2;
                    } else {
                        this.tabIndex = 'invalid-code';
                        this.isProceeding = false;
                    }
                })
                .catch(err => {
                    if(err.response.data && err.response.data.username) {
                        this.errors.username = err.response.data.username[0];
                        this.isProceeding = false;
                    } else {
                        this.tabIndex = 'invalid-code';
                        this.isProceeding = false;
                    }
                })
            },

            checkEmailAvailability() {
                axios.post('/api/v1.1/auth/invite/admin/ec', {
                    token: this.code,
                    email: this.form.email
                })
                .then(res => {
                    if(res && res.data) {
                        this.isProceeding = false;
                        this.tabIndex = 3;
                    } else {
                        this.tabIndex = 'invalid-code';
                        this.isProceeding = false;
                    }
                })
                .catch(err => {
                    if(err.response.data && err.response.data.email) {
                        this.errors.email = err.response.data.email[0];
                        this.isProceeding = false;
                    } else {
                        this.tabIndex = 'invalid-code';
                        this.isProceeding = false;
                    }
                })
            },

            validateEmail() {
                if(!this.form.email || !this.form.email.length) {
                    return false;
                }

                return /^[a-zA-Z]+[a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+[a-zA-Z]$/i.test(this.form.email);
            },

            handleRegistration() {
                var $form = $('<form>', {
                    action: '/api/v1.1/auth/invite/admin/re',
                    method: 'post'
                });
                let fields = {
                    '_token': document.head.querySelector('meta[name="csrf-token"]').content,
                    token: this.code,
                    username: this.form.username,
                    name: this.form.display_name,
                    email: this.form.email,
                    password: this.form.password,
                    password_confirm: this.form.password_confirm
                };

                $.each(fields, function(key, val) {
                     $('<input>').attr({
                         type: "hidden",
                         name: key,
                         value: val
                     }).appendTo($form);
                });
                $form.appendTo('body').submit();
            },

            proceed(cur) {
                this.isProceeding = true;
                event.currentTarget.blur();

                switch(cur) {
                    case 1:
                        this.checkUsernameAvailability();
                    break;

                    case 2:
                        this.checkEmailAvailability();
                    break;

                    case 3:
                        this.isProceeding = false;
                        this.tabIndex = 4;
                    break;

                    case 4:
                        this.isProceeding = false;
                        this.tabIndex = 5;
                    break;

                    case 5:
                        this.tabIndex = 6;
                        this.handleRegistration();
                    break;
                }
            }
        }
    }
</script>

<style lang="scss">
    .admin-invite-component {
        font-family: var(--font-family-sans-serif);

        &-inner {
            display: flex;
            width: 100wv;
            height: 100vh;
            justify-content: center;
            align-items: center;

            .card {
                width: 100%;
                color: #fff;
                padding: 1.25rem 2.5rem;
                border-radius: 10px;
                min-height: 530px;

                @media(min-width: 768px) {
                    width: 30%;
                }

                label {
                    color: var(--muted);
                    font-weight: bold;
                    text-transform: uppercase;
                }

                .login-link {
                    margin-top: 10px;
                    font-weight: 600;
                }

                .register-terms {
                    font-size: 12px;
                    color: var(--muted);
                }

                .form-control {
                    color: #fff;
                }

                .admin-message {
                    margin-top: 20px;
                    border: 1px solid var(--dropdown-item-hover-color);
                    color: var(--text-lighter);
                    padding: 1rem;
                    border-radius: 5px;
                }
            }
        }
    }
</style>