frontend first steps
This commit is contained in:
		
							parent
							
								
									65a67ae19b
								
							
						
					
					
						commit
						01a6481d62
					
				
							
								
								
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| VUE_APP_ROOT_WEBDAV="http://127.0.0.1:8080" | ||||
| @ -9,11 +9,15 @@ | ||||
|     "lint": "vue-cli-service lint" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "bootstrap": "^5.2.0", | ||||
|     "bootstrap-darkmode": "^5.0.1", | ||||
|     "bootstrap-icons": "^1.9.1", | ||||
|     "core-js": "^3.6.5", | ||||
|     "pinia": "^2.0.13", | ||||
|     "register-service-worker": "^1.7.1", | ||||
|     "typescript-is": "^0.19.0", | ||||
|     "vue": "^3.0.0", | ||||
|     "vue-router": "^4.0.0-0", | ||||
|     "pinia": "^2.0.13", | ||||
|     "webdav": "^4.9.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | ||||
							
								
								
									
										48
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								src/App.vue
									
									
									
									
									
								
							| @ -1,30 +1,24 @@ | ||||
| <template> | ||||
|   <div id="nav"> | ||||
|     <router-link to="/">Home</router-link> | | ||||
|     <router-link to="/about">About</router-link> | ||||
|   </div> | ||||
|   <router-view /> | ||||
|   <header | ||||
|     class="d-flex justify-content-between p-3 bg-darkmode-dark bg-light shadow" | ||||
|   > | ||||
|     <div> | ||||
|       <b class="mx-2">vuedav</b> | ||||
|       <router-link class="mx-2" to="/files">Files</router-link> | ||||
|     </div> | ||||
|     <div class="d-flex"> | ||||
|       <DarkModeToggle class="mx-2" v-slot="{ state }"> | ||||
|         <i v-if="state" class="bi-moon"></i> | ||||
|         <i v-else class="bi-sun"></i> | ||||
|       </DarkModeToggle> | ||||
|     </div> | ||||
|   </header> | ||||
|   <main class="container my-5"> | ||||
|     <router-view /> | ||||
|   </main> | ||||
|   <footer></footer> | ||||
| </template> | ||||
| 
 | ||||
| <style lang="scss"> | ||||
| #app { | ||||
|   font-family: Avenir, Helvetica, Arial, sans-serif; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
|   text-align: center; | ||||
|   color: #2c3e50; | ||||
| } | ||||
| 
 | ||||
| #nav { | ||||
|   padding: 30px; | ||||
| 
 | ||||
|   a { | ||||
|     font-weight: bold; | ||||
|     color: #2c3e50; | ||||
| 
 | ||||
|     &.router-link-exact-active { | ||||
|       color: #42b983; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| <script setup lang="ts"> | ||||
| import DarkModeToggle from '@/components/DarkmodeToggle.vue'; | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										23
									
								
								src/components/DarkmodeToggle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/DarkmodeToggle.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| <template> | ||||
|   <div class="form-check form-switch"> | ||||
|     <label class="custom-control-label" for="darkSwitch"> | ||||
|       <slot :state="sliderState" /> | ||||
|     </label> | ||||
|     <input v-model="sliderState" type="checkbox" class="form-check-input" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, watch } from 'vue'; | ||||
| import { themeConfig } from '@/main'; | ||||
| 
 | ||||
| const sliderState = ref(themeConfig.getTheme() === 'dark'); | ||||
| 
 | ||||
| watch(sliderState, (state) => { | ||||
|   themeConfig.setTheme(state ? 'dark' : 'light'); | ||||
| }); | ||||
| 
 | ||||
| themeConfig.themeChangeHandlers.push((newTheme) => { | ||||
|   sliderState.value = newTheme === 'dark'; | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										19
									
								
								src/components/SideMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/components/SideMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| <template> | ||||
|   <div class="card sideMenu sticky-top"> | ||||
|     <div class="card-header">Menu</div> | ||||
|     <div class="card-body"> | ||||
|       <router-link class="py-2" to="/files"> | ||||
|         <i class="bi-archive"></i> My Files | ||||
|       </router-link> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped lang="scss"> | ||||
| .sideMenu { | ||||
|   a { | ||||
|     color: inherit; | ||||
|     text-decoration: none; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										100
									
								
								src/components/files/FileList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/components/files/FileList.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| <template> | ||||
|   <div class="px-0"> | ||||
|     <div class="files card"> | ||||
|       <div class="card-header sticky-top bg-light bg-darkmode-dark"> | ||||
|         <div class="row"> | ||||
|           <i | ||||
|             v-if="path && path !== '/'" | ||||
|             @click="navigateUp" | ||||
|             class="col-1 px-0 btn btn-default bi-arrow-90deg-left" | ||||
|           ></i> | ||||
|           <PathSegments | ||||
|             class="col my-2" | ||||
|             :path="path" | ||||
|             @newPath="(newPath) => emit('newPath', newPath)" | ||||
|           /> | ||||
|           <div class="col-auto px-0"> | ||||
|             <FileUpload :path="path" @finished="fetchLocation" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="card-body"> | ||||
|         <div v-if="loading">loading...</div> | ||||
|         <div v-else> | ||||
|           <table class="table table-hover"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th></th> | ||||
|                 <th>Name</th> | ||||
|                 <th class="d-none d-md-table-cell">Modified</th> | ||||
|                 <th class="d-none d-sm-table-cell">Size</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <component | ||||
|                 v-for="file in files" | ||||
|                 :key="file" | ||||
|                 :is="file.type === 'file' ? FileListElement : FolderListElement" | ||||
|                 :file="file" | ||||
|                 :client="client" | ||||
|               /> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|         {{ error }} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import FileListElement from './FileListElement.vue'; | ||||
| import FolderListElement from './FolderListElement.vue'; | ||||
| import FileUpload from '@/components/helpers/FileUpload.vue'; | ||||
| import PathSegments from '@/components/files/PathSegments.vue'; | ||||
| import { useWebdavStore } from '@/store/webdav'; | ||||
| import { ref, defineProps, watch, defineEmits } from 'vue'; | ||||
| import type { File } from '@/models'; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   path: String, | ||||
| }); | ||||
| const emit = defineEmits(['newPath']); | ||||
| 
 | ||||
| const files = ref([] as Array<File>); | ||||
| const loading = ref(true as boolean); | ||||
| const error = ref('' as string); | ||||
| const store = useWebdavStore(); | ||||
| 
 | ||||
| const client = store.currentSession?.client; | ||||
| 
 | ||||
| const fetchLocation = async () => { | ||||
|   try { | ||||
|     loading.value = true; | ||||
|     files.value = (await client?.getDirectoryContents( | ||||
|       props.path ?? '/' | ||||
|     )) as Array<File>; | ||||
|     loading.value = false; | ||||
|     error.value = ''; | ||||
|   } catch (e) { | ||||
|     files.value = []; | ||||
|     loading.value = false; | ||||
|     error.value = e as string; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const navigateUp = () => { | ||||
|   const newPath = (props.path ?? '/').split('/'); | ||||
|   newPath.pop(); | ||||
|   emit('newPath', newPath.join('/') || '/'); | ||||
| }; | ||||
| 
 | ||||
| watch( | ||||
|   () => props.path, | ||||
|   () => { | ||||
|     fetchLocation(); | ||||
|   } | ||||
| ); | ||||
| 
 | ||||
| fetchLocation(); | ||||
| </script> | ||||
							
								
								
									
										32
									
								
								src/components/files/FileListElement.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/files/FileListElement.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| <template> | ||||
|   <tr> | ||||
|     <td><i class="icon col-1" :class="`bi-${getIconFromFile(file)}`"></i></td> | ||||
|     <td>{{ file.basename }}</td> | ||||
|     <td class="d-none d-md-table-cell"> | ||||
|       {{ new Date(file.lastmod).toLocaleDateString() }} | ||||
|     </td> | ||||
|     <td class="d-none d-sm-table-cell"><ByteCalc :bytes="file.size" /></td> | ||||
|   </tr> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ByteCalc from '@/components/helpers/ByteCalc.vue'; | ||||
| import { defineProps } from 'vue'; | ||||
| import { fileExtensions, defaultIcon } from '@/lib/fileTypeToIconMappings'; | ||||
| import type { File } from '@/models'; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   file: Object, | ||||
|   client: Object, | ||||
| }); | ||||
| 
 | ||||
| const getIconFromFile = (file: File): string => { | ||||
|   const segments = file.basename.split('.'); | ||||
|   if (segments.length < 2) return defaultIcon; | ||||
|   return fileExtensions.get(segments.pop() as string) ?? defaultIcon; | ||||
| }; | ||||
| 
 | ||||
| const downloadFile = async (file: File) => { | ||||
|   await props.client?.getFileContents(file.filename); | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										27
									
								
								src/components/files/FolderListElement.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/components/files/FolderListElement.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| <template> | ||||
|   <tr @click="$router.push(`#${file.filename}`)" class="file"> | ||||
|     <td><i class="icon bi-folder"></i></td> | ||||
|     <td>{{ file.basename }}</td> | ||||
|     <td class="d-none d-md-table-cell">{{ new Date(file.lastmod).toLocaleDateString() }}</td> | ||||
|     <td class="d-none d-sm-table-cell"></td> | ||||
|   </tr> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { defineProps } from 'vue'; | ||||
| 
 | ||||
| defineProps({ | ||||
|   file: Object, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss"> | ||||
| .file { | ||||
|   color: inherit !important; | ||||
|   text-decoration: inherit !important; | ||||
|   cursor: pointer; | ||||
|   &:hover{ | ||||
|     color: inherit !important; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										46
									
								
								src/components/files/PathSegments.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/files/PathSegments.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <span v-for="(segment, index) of getSegments().slice(0, -1)" :key="segment"> | ||||
|       <span | ||||
|         class="segment p-1" | ||||
|         @click=" | ||||
|           emit( | ||||
|             'newPath', | ||||
|             `/${getSegments() | ||||
|               .slice(1, index + 1) | ||||
|               .join('/')}` | ||||
|           ) | ||||
|         " | ||||
|         >{{ segment }}</span | ||||
|       ><i class="bi-caret-right mx-1"></i> | ||||
|     </span> | ||||
|     <span class="p-1"> | ||||
|       <b>{{ getSegments()?.pop() }}</b> | ||||
|     </span> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { defineProps, defineEmits } from 'vue'; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   path: String, | ||||
| }); | ||||
| const emit = defineEmits(['newPath']); | ||||
| 
 | ||||
| const getSegments = (): Array<string> => { | ||||
|   const segments = `My Files${props.path ?? ''}`.split('/'); | ||||
|   if (segments[segments.length - 1] === '') segments?.pop(); | ||||
|   return segments; | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style scoped lang="scss"> | ||||
| .segment { | ||||
|   border-radius: .2rem; | ||||
|   cursor: pointer; | ||||
|   &:hover { | ||||
|     background-color: #aaa6; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										23
									
								
								src/components/helpers/ByteCalc.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/helpers/ByteCalc.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| <template> | ||||
|   <span>{{ getByteString(bytes) }}</span> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { defineProps } from 'vue'; | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   bytes: Number, | ||||
| }); | ||||
| 
 | ||||
| const getByteString = (bytes: number): string => { | ||||
|   const unit = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] | ||||
|     .map((symbol, index) => ({ | ||||
|       symbol, | ||||
|       index, | ||||
|       breakpoint: 10 ** ((index + 1) * 3), | ||||
|     })) | ||||
|     .find((unit) => bytes < unit.breakpoint); | ||||
|   if (!unit) return 'wtf'; | ||||
|   return `${(bytes / 10 ** (unit.index * 3)).toFixed(2)} ${unit.symbol}`; | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										61
									
								
								src/components/helpers/FileUpload.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/components/helpers/FileUpload.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| <template> | ||||
|   <button class="btn btn-secondary" @click.prevent="$refs.fileInput.click()"> | ||||
|     <slot><i class="bi-plus"></i></slot> | ||||
|   </button> | ||||
|   <input | ||||
|     type="file" | ||||
|     ref="fileInput" | ||||
|     class="d-none" | ||||
|     @change="setFile($refs.fileInput.files[0])" | ||||
|   /> | ||||
|   <div v-if="info">{{ info }}</div> | ||||
|   <div v-if="progressRef"> | ||||
|     <ByteCalc :bytes="progressRef.loaded" /><span> of </span | ||||
|     ><ByteCalc :bytes="progressRef.total" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ByteCalc from '@/components/helpers/ByteCalc.vue'; | ||||
| import { defineEmits, defineProps, ref } from 'vue'; | ||||
| import { readFileBuffer } from '@/lib/readFileBlob'; | ||||
| import { useWebdavStore } from '@/store/webdav'; | ||||
| import type { ProgressEvent } from 'webdav'; | ||||
| 
 | ||||
| const props = defineProps({ path: String }); | ||||
| const emit = defineEmits(['started', 'finished', 'failed', 'progress']); | ||||
| const store = useWebdavStore(); | ||||
| 
 | ||||
| const progressRef = ref(null as null | ProgressEvent); | ||||
| const info = ref(''); | ||||
| 
 | ||||
| const setFile = async (file: File) => { | ||||
|   emit('started'); | ||||
|   const buffer = await readFileBuffer(file); | ||||
|   try { | ||||
|     await store.currentSession?.client.putFileContents( | ||||
|       `${props.path}/${file.name}`, | ||||
|       buffer, | ||||
|       { | ||||
|         onUploadProgress: (progress) => { | ||||
|           progressRef.value = progress; | ||||
|           emit('progress', progress); | ||||
|           console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`); | ||||
|         }, | ||||
|       } | ||||
|     ); | ||||
|     info.value = 'upload completed'; | ||||
|     emit('finished'); | ||||
|   } catch (e) { | ||||
|     info.value = 'upload failed'; | ||||
|     emit('failed', e); | ||||
|     console.error(e); | ||||
|   } | ||||
|   setTimeout(() => { | ||||
|     progressRef.value = null; | ||||
|     info.value = ''; | ||||
|   }, 2000); | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
							
								
								
									
										41
									
								
								src/lib/fileTypeToIconMappings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/lib/fileTypeToIconMappings.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| export const defaultIcon = 'file-earmark'; | ||||
| 
 | ||||
| export const fileExtensions = new Map([ | ||||
|   // Images
 | ||||
|   ['png', 'file-earmark-image'], | ||||
|   ['jpg', 'file-earmark-image'], | ||||
|   ['tiff', 'file-earmark-image'], | ||||
|   // Music
 | ||||
|   ['mp3', 'file-earmark-music'], | ||||
|   ['m4a', 'file-earmark-music'], | ||||
|   ['aac', 'file-earmark-music'], | ||||
|   ['aiff', 'file-earmark-music'], | ||||
|   ['wav', 'file-earmark-music'], | ||||
|   ['wma', 'file-earmark-music'], | ||||
|   // Code
 | ||||
|   ['html', 'file-earmark-code'], | ||||
|   ['htm', 'file-earmark-code'], | ||||
|   ['xml', 'file-earmark-code'], | ||||
|   ['js', 'file-earmark-code'], | ||||
|   ['mjs', 'file-earmark-code'], | ||||
|   ['py', 'file-earmark-code'], | ||||
|   ['sh', 'file-earmark-code'], | ||||
|   ['ts', 'file-earmark-code'], | ||||
|   ['go', 'file-earmark-code'], | ||||
|   ['rs', 'file-earmark-code'], | ||||
|   ['java', 'file-earmark-code'], | ||||
|   // Binaries
 | ||||
|   ['jar', 'file-earmark-binary'], | ||||
|   ['exe', 'file-earmark-binary'], | ||||
|   ['iso', 'file-earmark-binary'], | ||||
|   // etc...
 | ||||
|   ['pdf', 'file-earmark-pdf'], | ||||
|   ['txt', 'filetype-txt'], | ||||
|   ['zip', 'file-earmark-zip'], | ||||
|   ['gz', 'file-earmark-zip'], | ||||
|   ['xz', 'file-earmark-zip'], | ||||
| ]); | ||||
| 
 | ||||
| export const mimeTypes = new Map([ | ||||
|   ['application/pdf', 'file-earmark-pdf'], | ||||
| ]); | ||||
							
								
								
									
										41
									
								
								src/lib/readFileBlob.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/lib/readFileBlob.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| import { is } from 'typescript-is'; | ||||
| 
 | ||||
| export const readFileAs = <T>( | ||||
|   file: File, | ||||
|   getReaderMethod = (reader: FileReader) => reader.readAsDataURL | ||||
| ): Promise<T> => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const reader = new FileReader(); | ||||
|     reader.onerror = reject; | ||||
|     reader.onload = async (event) => { | ||||
|       const buffer = event.target?.result; | ||||
|       if (!buffer) reject('failed to read file'); | ||||
|       if (!is<T>(buffer)) reject('wrong type'); | ||||
|       else resolve(buffer as T); | ||||
|     }; | ||||
|     getReaderMethod(reader as FileReader)(file); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export const readFileBuffer = async (file: File): Promise<ArrayBuffer> => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const reader = new FileReader(); | ||||
|     reader.onerror = reject; | ||||
|     reader.onload = async (event) => { | ||||
|       const buffer = event.target?.result; | ||||
|       if (buffer === null || buffer === undefined || typeof buffer === 'string') | ||||
|         reject('failed to read file'); | ||||
|       else resolve(buffer); | ||||
|     }; | ||||
|     reader.readAsArrayBuffer(file); | ||||
|   }); | ||||
|   /*return readFileAs<ArrayBuffer>( | ||||
|     file, | ||||
|     (reader: FileReader) => reader.readAsArrayBuffer | ||||
|   );*/ | ||||
| }; | ||||
| 
 | ||||
| export const readFileBlob = async (file: File): Promise<Blob> => { | ||||
|   const data = await readFileAs<string>(file); | ||||
|   return await (await fetch(data)).blob(); | ||||
| }; | ||||
| @ -1,29 +1,29 @@ | ||||
| import { Context } from '@/middleware/Context'; | ||||
| 
 | ||||
| type RunMiddleware = (context: Context) => void; | ||||
| type Middleware = (context: Context) => void; | ||||
| function nextFactory( | ||||
|   context: Context, | ||||
|   middlewares: Array<RunMiddleware>, | ||||
|   middlewares: Array<Middleware>, | ||||
|   index: number | ||||
| ) { | ||||
|   const subsequentMiddleware = middlewares[index]; | ||||
|   if (!subsequentMiddleware) return context.next; | ||||
| 
 | ||||
|   return (...args: any[]) => { | ||||
|   return (...args: Array<any>) => { | ||||
|     // @ts-ignore
 | ||||
|     context.next(...args); | ||||
|     context.next(...args as Array<any>); | ||||
|     subsequentMiddleware({ | ||||
|       ...context, | ||||
|       next: nextFactory(context, middlewares, index++), | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
| export function runMiddleware(context: Context) { | ||||
| export function runMiddleware(context: Context): void { | ||||
|   const { to } = context; | ||||
|   const middlewares = [ | ||||
|     ...((Array.isArray(to.meta.middleware) | ||||
|       ? to.meta.middleware | ||||
|       : [to.meta.middleware]) as Array<RunMiddleware>), | ||||
|       : [to.meta.middleware]) as Array<Middleware>), | ||||
|   ]; | ||||
| 
 | ||||
|   middlewares[0]({ ...context, next: nextFactory(context, middlewares, 1) }); | ||||
|  | ||||
							
								
								
									
										16
									
								
								src/main.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/main.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| // Your variable overrides | ||||
| //$body-bg: #222; | ||||
| //$body-color: #222; | ||||
| $dark-body-bg: #222; | ||||
| 
 | ||||
| // Bootstrap and its default variables | ||||
| @import "~bootstrap/scss/bootstrap"; | ||||
| @import "~bootstrap-darkmode/css/darktheme"; | ||||
| 
 | ||||
| html, body { | ||||
| } | ||||
| 
 | ||||
| #app{ | ||||
|   font-family: monospace; | ||||
|   //color: #fff; | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/main.ts
									
									
									
									
									
								
							| @ -1,7 +1,13 @@ | ||||
| import { createApp } from "vue"; | ||||
| import App from "./App.vue"; | ||||
| import "./registerServiceWorker"; | ||||
| import router from "./router"; | ||||
| import store from "./store"; | ||||
| import { createApp } from 'vue'; | ||||
| import App from './App.vue'; | ||||
| import './registerServiceWorker'; | ||||
| import router from './router'; | ||||
| import store from './store'; | ||||
| import 'bootstrap-icons/font/bootstrap-icons.scss'; | ||||
| import './main.scss'; | ||||
| import { ThemeConfig, writeDarkSwitch } from 'bootstrap-darkmode'; | ||||
| 
 | ||||
| createApp(App).use(store).use(router).mount("#app"); | ||||
| export const themeConfig = new ThemeConfig(); | ||||
| themeConfig.initTheme(); | ||||
| 
 | ||||
| createApp(App).use(store).use(router).mount('#app'); | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| import { useWebdavStorage } from '@/store/webdav'; | ||||
| import { useWebdavStore } from '@/store/webdav'; | ||||
| import { Context } from '@/middleware/Context'; | ||||
| 
 | ||||
| export default function auth({ next, router }: Context) { | ||||
|   console.log('auth'); | ||||
|   if (!useWebdavStorage().currentSession?.isActive) return router.push({ name: 'Login' }); | ||||
| export const auth = ({ next }: Context) => { | ||||
|   if (!useWebdavStore().currentSession?.isActive) | ||||
|     return next({ name: 'Login' }); | ||||
|   return next(); | ||||
| } | ||||
| }; | ||||
|  | ||||
							
								
								
									
										9
									
								
								src/models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/models.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| export type File = { | ||||
|   basename: string; | ||||
|   etag: string; | ||||
|   filename: string; | ||||
|   lastmod: string; | ||||
|   mime: string; | ||||
|   size: number; | ||||
|   type: string; | ||||
| }; | ||||
| @ -1,7 +1,6 @@ | ||||
| import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; | ||||
| import Home from '../views/Home.vue'; | ||||
| import auth from '@/middleware/auth'; | ||||
| import log from '@/middleware/log'; | ||||
| import { auth } from '@/middleware/auth'; | ||||
| import { runMiddleware } from '@/lib/runMiddleware'; | ||||
| 
 | ||||
| const routes: Array<RouteRecordRaw> = [ | ||||
| @ -22,9 +21,6 @@ const routes: Array<RouteRecordRaw> = [ | ||||
|     path: '/login', | ||||
|     name: 'Login', | ||||
|     component: () => import('../views/Login.vue'), | ||||
|     meta: { | ||||
|       middleware: [log], | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
| @ -1,13 +0,0 @@ | ||||
| import {defineStore} from "pinia"; | ||||
| 
 | ||||
| export const useAuthStorage = defineStore('auth', { | ||||
|   state: () => ({ | ||||
|     user: '', | ||||
|     password: '' | ||||
|   }), | ||||
|   actions: { | ||||
|     login(){ | ||||
| 
 | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| @ -11,31 +11,29 @@ export type Session = { | ||||
|   isActive: boolean; | ||||
| }; | ||||
| 
 | ||||
| export const useWebdavStorage = defineStore('auth', { | ||||
| export const useWebdavStore = defineStore('auth', { | ||||
|   state: () => ({ | ||||
|     sessions: [] as Array<Session>, | ||||
|     currentSession: null as null | Session, | ||||
|   }), | ||||
|   actions: { | ||||
|     login({ user, pass }: Auth): Promise<Session> { | ||||
|       return new Promise((resolve, reject) => { | ||||
|         try { | ||||
|           const client = createClient( | ||||
|             process.env.VUE_APP_ROOT_WEBDAV as string, | ||||
|             { | ||||
|               authType: AuthType.Digest, | ||||
|               username: user as string, | ||||
|               password: pass as string, | ||||
|             } | ||||
|           ) as WebDAVClient; | ||||
|           const session = { client, isActive: true } as Session; | ||||
|           this.sessions.push(session); | ||||
|           this.currentSession = session; | ||||
|           resolve(session); | ||||
|         } catch (e) { | ||||
|           reject(e); | ||||
|         } | ||||
|       }); | ||||
|     async login({ user, pass }: Auth): Promise<Session> { | ||||
|       try { | ||||
|         const client = createClient( | ||||
|           `${process.env.VUE_APP_ROOT_WEBDAV ?? ''}/api/dav/files/` as string, | ||||
|           { | ||||
|             authType: AuthType.Password, | ||||
|             username: user as string, | ||||
|             password: pass as string, | ||||
|           } | ||||
|         ) as WebDAVClient; | ||||
|         const session = { client, isActive: true } as Session; | ||||
|         this.sessions.push(session); | ||||
|         this.currentSession = session; | ||||
|         return session; | ||||
|       } catch (e) { | ||||
|         throw 'login failed'; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @ -1,11 +1,20 @@ | ||||
| <template> | ||||
|   <h1>Files</h1> | ||||
|   <div class="row mx-1 justify-content-center"> | ||||
|     <div class="d-none d-lg-block col-3"> | ||||
|       <SideMenu /> | ||||
|     </div> | ||||
|     <FileList | ||||
|       class="col" | ||||
|       :path="$route.hash.slice(1)" | ||||
|       @newPath="(path) => $router.push(`#${path}`)" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'Files', | ||||
| }; | ||||
| </script> | ||||
| <script setup lang="ts"> | ||||
| import FileList from '@/components/files/FileList.vue'; | ||||
| import SideMenu from '@/components/SideMenu.vue'; | ||||
| import { useWebdavStore } from '@/store/webdav'; | ||||
| 
 | ||||
| <style scoped></style> | ||||
| const store = useWebdavStore(); | ||||
| </script> | ||||
|  | ||||
| @ -2,22 +2,32 @@ | ||||
|   <h1>Login</h1> | ||||
|   user: <input type="text" v-model="user" /><br /> | ||||
|   pass: <input type="password" v-model="pass" /><br /> | ||||
|   <button @click="login">Login</button> | ||||
|   <button class="btn btn-primary" @click="login">Login</button><br /> | ||||
|   <span class="error">{{ error }}</span> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import { useWebdavStorage } from '@/store/webdav'; | ||||
| import { useWebdavStore } from '@/store/webdav'; | ||||
| import { useRouter } from 'vue-router'; | ||||
| 
 | ||||
| const user = ref(''); | ||||
| const pass = ref(''); | ||||
| const error = ref(''); | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| const store = useWebdavStore(); | ||||
| 
 | ||||
| const login = async () => { | ||||
|   await useWebdavStorage() | ||||
|     .login({ user, pass }) | ||||
|     .then(async session => { | ||||
|       console.log(await session.client.getDirectoryContents('/')); | ||||
|     }); | ||||
|   try { | ||||
|     await store.login({ user: user.value, pass: pass.value }); | ||||
|     error.value = ''; | ||||
|     await router.push({ name: 'Files' }); | ||||
|   } catch (e) { | ||||
|     console.error(e); | ||||
|     error.value = e as string; | ||||
|   } | ||||
|   await store.currentSession?.client.getDirectoryContents(`/`); | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user