Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Moonraker user authorization #370

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ docker/config/*.cfg
docker/config/*.conf
docker/config/.mainsail.json
docker/config/.theme
*.iml
57 changes: 57 additions & 0 deletions src/api/moonraker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import axios from 'axios'
import store from '@/store'

import {
MoonrakerApiResponse, MoonrakerCreateUserResponse, MoonrakerDeleteUserResponse,
MoonrakerLoginResponse,
MoonrakerUser,
MoonrakerUsersResponse
} from '@/api/moonraker.types'

async function login(baseUrl: string, username: string, password: string): Promise<MoonrakerLoginResponse> {
const url = baseUrl + '/access/login'
const response = await axios.post<MoonrakerApiResponse<MoonrakerLoginResponse>>(url, {
username,
password
})
return response.data.result
}

export async function getOneshotToken(baseUrl: string): Promise<string> {
const url = `${baseUrl}/access/oneshot_token`
const response = await axios.get<MoonrakerApiResponse<string>>(url, {})
return response.data.result
}

async function getUserlist(baseUrl: string): Promise<MoonrakerUser[]> {
const url = baseUrl + '/access/users/list'
const result = await axios.get<MoonrakerApiResponse<MoonrakerUsersResponse>>(url, {})
return result.data.result.users
}

async function deleteUser(baseUrl: string, username: string): Promise<MoonrakerDeleteUserResponse> {
const url = store.getters['socket/getUrl'] + '/access/user'
const response = await axios.delete<MoonrakerApiResponse<MoonrakerDeleteUserResponse>>(url, {
data: {
username
}
})
return response.data.result
}

async function createUser(baseUrl: string, username: string, password: string): Promise<MoonrakerCreateUserResponse> {
const url = baseUrl + '/access/user'
const response = await axios.post<MoonrakerApiResponse<MoonrakerCreateUserResponse>>(url, {
username: username,
password: password
}, {})
return response.data.result
}

export const MoonrakerApi = {
login,
createUser,
deleteUser,
getOneshotToken,
getUserlist
}
40 changes: 40 additions & 0 deletions src/api/moonraker.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export interface MoonrakerApiResponse<T> {
result: T
}

export interface MoonrakerDeleteUserResponse {
username: string
action: 'user_deleted'
}

export interface MoonrakerCreateUserResponse {
username: string
password: string
}

export interface MoonrakerLoginResponse {
username: string
token: string
refresh_token: string
action: 'user_logged_in'
}

export interface MoonrakerLogoutResponse {
username: string;
action: 'user_logged_out'
}

export interface MoonrakerUsersResponse {
users: MoonrakerUser[]
}

export interface MoonrakerUser {
username: string
created_on: number
}

export interface RefreshTokenResponse {
username: string;
token: string;
action: 'user_jwt_refresh'
}
29 changes: 25 additions & 4 deletions src/components/TheConnectingDialog.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<style scoped>

</style>

<template>
Expand All @@ -9,7 +8,8 @@
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>
<template v-if="connectingFailed">{{ $t("ConnectionDialog.Failed", {'host': formatHostname}) }}</template>
<template v-if="showLogin">{{ $t("ConnectionDialog.Login", {'host': formatHostname}) }}</template>
<template v-else-if="!isConnecting && connectingFailed">{{ $t("ConnectionDialog.Failed", {'host': formatHostname}) }}</template>
<template v-else-if="isConnecting">{{ $t("ConnectionDialog.Connecting", {'host': formatHostname}) }}</template>
<template v-else>{{ formatHostname }}</template>
</span>
Expand All @@ -18,7 +18,7 @@
<v-card-text class="pt-5" v-if="isConnecting">
<v-progress-linear color="white" indeterminate></v-progress-linear>
</v-card-text>
<v-card-text class="pt-5" v-if="!isConnecting && connectingFailed">
<v-card-text class="pt-5" v-if="!isConnecting && !showLogin && connectingFailed">
<connection-status :moonraker="false"></connection-status>
<p class="text-center mt-3">{{ $t("ConnectionDialog.CannotConnectTo", {'host': formatHostname}) }}</p>
<template v-if="counter > 2">
Expand All @@ -34,6 +34,9 @@
<v-btn @click="reconnect" color="primary">{{ $t("ConnectionDialog.TryAgain") }}</v-btn>
</div>
</v-card-text>

<login-form v-if="showLogin" :show-error="loginFailed" @login="login"></login-form>

</v-card>
</v-dialog>
</template>
Expand All @@ -44,10 +47,13 @@ import Component from 'vue-class-component'
import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import ConnectionStatus from '@/components/ui/ConnectionStatus.vue'
import LoginForm from '@/components/ui/LoginForm.vue'
import {UserCredentials} from '@/store/auth/types'

@Component({
components: {
ConnectionStatus,
LoginForm
}
})
export default class TheUpdateDialog extends Mixins(BaseMixin) {
Expand Down Expand Up @@ -85,10 +91,25 @@ export default class TheUpdateDialog extends Mixins(BaseMixin) {
return !this.isConnected
}

get showLogin() {
return this.$store.state.auth.showLogin
}

get loginFailed() {
return this.$store.state.auth.loginFailed
}

reconnect() {
this.counter++
this.$store.dispatch('socket/setData', { connectingFailed: false })
this.$socket.connect()
}

login(user: UserCredentials) {
this.$store.dispatch('auth/login', {
username: user.username,
password: user.password
})
}
}
</script>
</script>
58 changes: 51 additions & 7 deletions src/components/panels/FarmPrinterPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,27 @@
</v-card-text>
<v-fade-transition>
<v-overlay v-if="hover" absolute :z-index="4" >
<v-btn color="primary" @click="clickPrinter">{{ printer.socket.isConnected ? $t("Panels.FarmPrinterPanel.SwitchToPrinter") : $t("Panels.FarmPrinterPanel.ReconnectToPrinter") }}</v-btn>
<v-btn color="primary" @click="clickPrinter">{{ $t(getButtonText) }}</v-btn>
</v-overlay>
</v-fade-transition>
</div>
</template>
</v-hover>

<v-dialog v-model="showLogin" persistent :width="400">
<v-card dark>
<v-toolbar flat dense color="primary">
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>
<template>{{ $t("ConnectionDialog.Login", {'host': printer_name}) }}</template>
</span>
</v-toolbar-title>
</v-toolbar>
<login-form :show-error="printer.socket.loginFailed" @login="login"></login-form>
</v-card>
</v-dialog>

</v-card>
</template>

Expand All @@ -122,17 +137,20 @@ import { FarmPrinterState } from '@/store/farm/printer/types'
import Mjpegstreamer from '@/components/webcams/Mjpegstreamer.vue'
import MjpegstreamerAdaptive from '@/components/webcams/MjpegstreamerAdaptive.vue'
import MainsailLogo from '@/components/ui/MainsailLogo.vue'
import LoginForm from '@/components/ui/LoginForm.vue'
import {UserCredentials} from '@/store/auth/types'

@Component({
components: {
'webcam-mjpegstreamer': Mjpegstreamer,
'webcam-mjpegstreamer-adaptive': MjpegstreamerAdaptive,
'mainsail-logo': MainsailLogo
'mainsail-logo': MainsailLogo,
LoginForm
}
})
export default class FarmPrinterPanel extends Mixins(BaseMixin) {
private imageHeight = 200;

private showLogin = false
@Prop({ type: Object, required: true }) printer!: FarmPrinterState
@Ref() readonly imageDiv!: Vue

Expand Down Expand Up @@ -166,6 +184,10 @@ export default class FarmPrinterPanel extends Mixins(BaseMixin) {
return this.$store.getters['farm/'+this.printer._namespace+'/getStatus']
}

get reqiresLogin() {
return this.$store.getters['farm/'+this.printer._namespace+'/getRequiresLogin']
}

get printer_current_filename() {
return this.$store.getters['farm/'+this.printer._namespace+'/getCurrentFilename']
}
Expand Down Expand Up @@ -201,11 +223,33 @@ export default class FarmPrinterPanel extends Mixins(BaseMixin) {
return false
}

get getButtonText(): string {
if (this.printer.socket.isConnected) {
return 'Panels.FarmPrinterPanel.SwitchToPrinter'
}
if (this.printer.socket.requiresLogin) {
return 'Panels.FarmPrinterPanel.Login'
}

return 'Panels.FarmPrinterPanel.ReconnectToPrinter'
}

clickPrinter() {
if (this.printer.socket.isConnected)
if (this.printer.socket.requiresLogin) {
this.showLogin = true
return
}
if (this.printer.socket.isConnected) {
this.$store.dispatch('changePrinter', { printer: this.printer._namespace })
else
this.$store.dispatch('farm/'+this.printer._namespace+'/reconnect')
return
}
this.$store.dispatch('farm/'+this.printer._namespace+'/reconnect')
}

login(user: UserCredentials) {
this.$store.dispatch('farm/'+this.printer._namespace+'/login', user).then(() => {
this.showLogin = this.printer.socket.loginFailed
})
}

mounted() {
Expand All @@ -224,4 +268,4 @@ export default class FarmPrinterPanel extends Mixins(BaseMixin) {
}

}
</script>
</script>
2 changes: 1 addition & 1 deletion src/components/panels/Machine/ConfigFilesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<template>
<div>
<v-card>
<v-card class="mb-6">
<v-toolbar flat dense>
<v-toolbar-title>
<span class="subheading align-baseline"><v-icon left>mdi-information</v-icon>{{ $t('Machine.ConfigFilesPanel.ConfigFiles') }}</span>
Expand Down
Loading