Added basic login.
This commit is contained in:
		
							parent
							
								
									76a831eb50
								
							
						
					
					
						commit
						78b389e33a
					
				|  | @ -2,4 +2,5 @@ node_modules/ | |||
| dist/ | ||||
| config/* | ||||
| !config/default.*js | ||||
| log | ||||
| log/ | ||||
| media/ | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
| 
 | ||||
|     &:focus { | ||||
| 		outline: none; | ||||
| 		border-color: var(--primary-faded); | ||||
| 		border-color: var(--primary-light-10); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -52,7 +52,7 @@ | |||
| } | ||||
| 
 | ||||
| .button-submit { | ||||
|     background: var(--primary-light-30); | ||||
|     background: var(--primary-light-10); | ||||
|     color: var(--text-light); | ||||
| 
 | ||||
|     &:hover:not(:disabled) { | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| :root { | ||||
|     --primary: #f65596; | ||||
|     --primary-strong: #f90071; | ||||
|     --primary-faded: #ffcce4; | ||||
|     --primary-light-10: #f075a6; | ||||
| 
 | ||||
|     --grey-dark-50: #111; | ||||
|     --grey-dark-40: #222; | ||||
|  |  | |||
|  | @ -1,216 +1,194 @@ | |||
| .resize-observer[data-v-b329ee4c] { | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	z-index: -1; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	border: none; | ||||
| 	background-color: transparent; | ||||
| 	pointer-events: none; | ||||
| 	display: block; | ||||
| 	overflow: hidden; | ||||
| 	opacity: 0 | ||||
| } | ||||
| 
 | ||||
| .resize-observer[data-v-b329ee4c] object { | ||||
| 	display: block; | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	height: 100%; | ||||
| 	width: 100%; | ||||
| 	overflow: hidden; | ||||
| 	pointer-events: none; | ||||
| 	z-index: -1 | ||||
| } | ||||
| /* Content */ | ||||
| 
 | ||||
| .v-popper__popper { | ||||
| 	z-index: 10000; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	outline: none; | ||||
| 	max-height: 100%; | ||||
|   z-index: 10000; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper.v-popper__popper--hidden { | ||||
| 	visibility: hidden; | ||||
| 	opacity: 0; | ||||
| 	transition: opacity .15s, visibility .15s; | ||||
| 	pointer-events: none | ||||
|   visibility: hidden; | ||||
|   opacity: 0; | ||||
|   transition: opacity .15s, visibility .15s; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper.v-popper__popper--shown { | ||||
| 	visibility: visible; | ||||
| 	opacity: 1; | ||||
| 	transition: opacity .15s | ||||
|   visibility: visible; | ||||
|   opacity: 1; | ||||
|   transition: opacity .15s; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper.v-popper__popper--skip-transition, | ||||
| .v-popper__popper.v-popper__popper--skip-transition>.v-popper__wrapper { | ||||
| 	transition: none !important | ||||
| .v-popper__popper.v-popper__popper--skip-transition > .v-popper__wrapper { | ||||
|   transition: none !important; | ||||
| } | ||||
| 
 | ||||
| .v-popper__backdrop { | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	display: none | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .v-popper__inner { | ||||
| 	position: relative; | ||||
| 	box-sizing: border-box; | ||||
| 	overflow-y: auto | ||||
|   position: relative; | ||||
|   box-sizing: border-box; | ||||
|   overflow-y: auto; | ||||
| } | ||||
| 
 | ||||
| .v-popper__inner>div { | ||||
| 	position: relative; | ||||
| 	z-index: 1; | ||||
| 	max-width: inherit; | ||||
| 	max-height: inherit | ||||
| .v-popper__inner > div { | ||||
|   position: relative; | ||||
|   z-index: 1; | ||||
|   max-width: inherit; | ||||
|   max-height: inherit; | ||||
| } | ||||
| 
 | ||||
| .v-popper__arrow-container { | ||||
| 	position: absolute; | ||||
| 	width: 10px; | ||||
| 	height: 10px | ||||
|   position: absolute; | ||||
|   width: 10px; | ||||
|   height: 10px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper--arrow-overflow .v-popper__arrow-container, | ||||
| .v-popper__popper--no-positioning .v-popper__arrow-container { | ||||
| 	display: none | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .v-popper__arrow-inner, | ||||
| .v-popper__arrow-outer { | ||||
| 	border-style: solid; | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	width: 0; | ||||
| 	height: 0 | ||||
|   border-style: solid; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 0; | ||||
|   height: 0; | ||||
| } | ||||
| 
 | ||||
| .v-popper__arrow-inner { | ||||
| 	visibility: hidden; | ||||
| 	border-width: 7px | ||||
|   visibility: hidden; | ||||
|   border-width: 7px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__arrow-outer { | ||||
| 	border-width: 6px | ||||
|   border-width: 6px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=top] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-inner { | ||||
| 	left: -2px | ||||
| .v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner { | ||||
|   left: -2px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=top] .v-popper__arrow-outer, | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-outer { | ||||
| 	left: -1px | ||||
| .v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-outer, | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer { | ||||
|   left: -1px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=top] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=top] .v-popper__arrow-outer { | ||||
| 	border-bottom-width: 0; | ||||
| 	border-left-color: transparent !important; | ||||
| 	border-right-color: transparent !important; | ||||
| 	border-bottom-color: transparent !important | ||||
| .v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-outer { | ||||
|   border-bottom-width: 0; | ||||
|   border-left-color: transparent !important; | ||||
|   border-right-color: transparent !important; | ||||
|   border-bottom-color: transparent !important; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=top] .v-popper__arrow-inner { | ||||
| 	top: -2px | ||||
| .v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner { | ||||
|   top: -2px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-container { | ||||
| 	top: 0 | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-container { | ||||
|   top: 0; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-outer { | ||||
| 	border-top-width: 0; | ||||
| 	border-left-color: transparent !important; | ||||
| 	border-right-color: transparent !important; | ||||
| 	border-top-color: transparent !important | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer { | ||||
|   border-top-width: 0; | ||||
|   border-left-color: transparent !important; | ||||
|   border-right-color: transparent !important; | ||||
|   border-top-color: transparent !important; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-inner { | ||||
| 	top: -4px | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner { | ||||
|   top: -4px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-outer { | ||||
| 	top: -6px | ||||
| .v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer { | ||||
|   top: -6px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-inner { | ||||
| 	top: -2px | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner { | ||||
|   top: -2px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-outer, | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-outer { | ||||
| 	top: -1px | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-outer, | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer { | ||||
|   top: -1px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-outer { | ||||
| 	border-left-width: 0; | ||||
| 	border-left-color: transparent !important; | ||||
| 	border-top-color: transparent !important; | ||||
| 	border-bottom-color: transparent !important | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer { | ||||
|   border-left-width: 0; | ||||
|   border-left-color: transparent !important; | ||||
|   border-top-color: transparent !important; | ||||
|   border-bottom-color: transparent !important; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-inner { | ||||
| 	left: -4px | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner { | ||||
|   left: -4px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=right] .v-popper__arrow-outer { | ||||
| 	left: -6px | ||||
| .v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer { | ||||
|   left: -6px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-container { | ||||
| 	right: -10px | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-container { | ||||
|   right: -10px; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-outer { | ||||
| 	border-right-width: 0; | ||||
| 	border-top-color: transparent !important; | ||||
| 	border-right-color: transparent !important; | ||||
| 	border-bottom-color: transparent !important | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner, | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-outer { | ||||
|   border-right-width: 0; | ||||
|   border-top-color: transparent !important; | ||||
|   border-right-color: transparent !important; | ||||
|   border-bottom-color: transparent !important; | ||||
| } | ||||
| 
 | ||||
| .v-popper__popper[data-popper-placement^=left] .v-popper__arrow-inner { | ||||
| 	left: -2px | ||||
| .v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner { | ||||
|   left: -2px; | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__inner { | ||||
| 	background: #fff; | ||||
| 	color: #000; | ||||
| 	border-radius: 6px; | ||||
| 	border: 1px solid #ddd; | ||||
| 	box-shadow: 0 6px 30px #0000001a | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__arrow-inner { | ||||
| 	visibility: visible; | ||||
| 	border-color: #fff | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__arrow-outer { | ||||
| 	border-color: #ddd | ||||
| } | ||||
| /* Tooltip */ | ||||
| 
 | ||||
| .v-popper--theme-tooltip .v-popper__inner { | ||||
| 	background: rgba(0, 0, 0, .8); | ||||
| 	color: #fff; | ||||
| 	border-radius: 6px; | ||||
| 	padding: 7px 12px 6px | ||||
|   background: rgba(0, 0, 0, .8); | ||||
|   color: white; | ||||
|   border-radius: 6px; | ||||
|   padding: 7px 12px 6px; | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-tooltip .v-popper__arrow-outer { | ||||
| 	border-color: #000c | ||||
|   border-color: rgba(0, 0, 0, .8); | ||||
| } | ||||
| 
 | ||||
| /* Dropdown */ | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__inner { | ||||
|   background: #fff; | ||||
|   color: black; | ||||
|   border-radius: 6px; | ||||
|   border: 1px solid #ddd; | ||||
|   box-shadow: 0 6px 30px rgba(0, 0, 0, .1); | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__arrow-inner { | ||||
|   visibility: visible; | ||||
|   border-color: #fff; | ||||
| } | ||||
| 
 | ||||
| .v-popper--theme-dropdown .v-popper__arrow-outer { | ||||
|   border-color: #ddd; | ||||
| } | ||||
|  |  | |||
|  | @ -53,36 +53,85 @@ | |||
| 			</ul> | ||||
| 		</nav> | ||||
| 
 | ||||
| 		<form | ||||
| 			class="search" | ||||
| 			@submit.prevent="search" | ||||
| 		> | ||||
| 			<input | ||||
| 				v-model="query" | ||||
| 				type="search" | ||||
| 				placeholder="Search" | ||||
| 				class="input" | ||||
| 		<div class="header-section"> | ||||
| 			<form | ||||
| 				class="search" | ||||
| 				@submit.prevent="search" | ||||
| 			> | ||||
| 				<input | ||||
| 					v-model="query" | ||||
| 					type="search" | ||||
| 					placeholder="Search" | ||||
| 					class="input" | ||||
| 				> | ||||
| 
 | ||||
| 			<Icon icon="search" /> | ||||
| 		</form> | ||||
| 				<Icon icon="search" /> | ||||
| 			</form> | ||||
| 
 | ||||
| 			<VDropdown | ||||
| 				v-if="user" | ||||
| 				:triggers="['click']" | ||||
| 			> | ||||
| 				<div class="userpanel"> | ||||
| 					<img | ||||
| 						:src="user.avatar" | ||||
| 						class="avatar" | ||||
| 					> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<template #popper> | ||||
| 					<div class="menu"> | ||||
| 						<a | ||||
| 							:href="`/user/${user.username}`" | ||||
| 							class="menu-header" | ||||
| 						>{{ user.username }}</a> | ||||
| 
 | ||||
| 						<ul class="menu-list nolist"> | ||||
| 							<li | ||||
| 								class="menu-item logout" | ||||
| 								@click="logout" | ||||
| 							><Icon icon="exit2" />Log out</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</VDropdown> | ||||
| 
 | ||||
| 			<div | ||||
| 				v-else | ||||
| 				class="userpanel" | ||||
| 			> | ||||
| 				<a | ||||
| 					:href="`/login?r=${encodeURIComponent(currentPath)}`" | ||||
| 					class="login button button-submit" | ||||
| 				>Log in</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</header> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, computed, inject } from 'vue'; | ||||
| import navigate from '#/src/navigate.js'; | ||||
| import { del } from '#/src/api.js'; | ||||
| 
 | ||||
| import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved | ||||
| 
 | ||||
| const pageContext = inject('pageContext'); | ||||
| 
 | ||||
| const user = pageContext.user; | ||||
| const query = ref(pageContext.urlParsed.search.q || ''); | ||||
| 
 | ||||
| const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]); | ||||
| const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`; | ||||
| 
 | ||||
| function search() { | ||||
| 	navigate('/search', { q: query.value }, { redirect: true }); | ||||
| } | ||||
| 
 | ||||
| async function logout() { | ||||
| 	del('/session'); | ||||
| 	window.location.reload(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
|  | @ -134,18 +183,22 @@ function search() { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .search { | ||||
| .header-section { | ||||
| 	height: 100%; | ||||
| 	display: flex; | ||||
| 	align-items: stretch; | ||||
| } | ||||
| 
 | ||||
| .search { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	flex-direction: row-reverse; | ||||
| 	flex-direction: row-reverse; /* allow icon to be selected */ | ||||
| 
 | ||||
| 	.input { | ||||
| 		height: 100%; | ||||
| 		border: none; | ||||
| 		border-radius: 0; | ||||
| 		border-left: solid 1px var(--shadow-weak-30); | ||||
| 		padding: .5rem 1rem; | ||||
| 		border-radius: 1rem; | ||||
| 		background: var(--background); | ||||
| 		margin: 0; | ||||
| 	} | ||||
| 
 | ||||
| 	.icon { | ||||
|  | @ -157,4 +210,63 @@ function search() { | |||
| 		fill: var(--primary); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .userpanel { | ||||
| 	height: 100%; | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: 0 1rem; | ||||
| 	font-size: 0; | ||||
| 	cursor: pointer; | ||||
| 
 | ||||
| 	&:hover .avatar { | ||||
| 		box-shadow: 0 0 3px var(--shadow-weak-10); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .avatar { | ||||
| 	width: 2rem; | ||||
| 	height: 2rem; | ||||
| 	border-radius: .25rem; | ||||
| 	object-fit: cover; | ||||
| } | ||||
| 
 | ||||
| .login { | ||||
| 	text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .menu-header { | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| 	padding: .75rem 1rem; | ||||
| 	border-bottom: solid 1px var(--shadow-weak-30); | ||||
| 	color: var(--shadow-strong-30); | ||||
| 	text-decoration: none; | ||||
| 	text-align: center; | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .menu-item { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: .5rem; | ||||
| 
 | ||||
| 	.icon { | ||||
| 		fill: var(--shadow); | ||||
| 		margin-right: .5rem; | ||||
| 	} | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		background: var(--shadow-weak-30); | ||||
| 		cursor: pointer; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .logout { | ||||
| 	color: var(--error); | ||||
| 
 | ||||
| 	.icon { | ||||
| 		fill: var(--error); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
								
								
								
								
							
							
						
						
									
										10
									
								
								package.json
								
								
								
								
							|  | @ -8,8 +8,11 @@ | |||
|   }, | ||||
|   "dependencies": { | ||||
|     "@brillout/json-serializer": "^0.5.8", | ||||
|     "@dicebear/collection": "^7.0.5", | ||||
|     "@dicebear/core": "^7.0.5", | ||||
|     "@floating-ui/dom": "^1.5.3", | ||||
|     "@floating-ui/vue": "^1.0.2", | ||||
|     "@resvg/resvg-js": "^2.6.0", | ||||
|     "@toycode/markdown-it-class": "^1.2.4", | ||||
|     "@vitejs/plugin-vue": "^4.5.2", | ||||
|     "@vue/compiler-sfc": "^3.3.10", | ||||
|  | @ -18,6 +21,7 @@ | |||
|     "@vueuse/core": "^10.7.1", | ||||
|     "compression": "^1.7.4", | ||||
|     "config": "^3.3.9", | ||||
|     "connect-redis": "^7.1.1", | ||||
|     "convert": "^4.14.1", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "date-fns": "^3.0.0", | ||||
|  | @ -25,6 +29,8 @@ | |||
|     "express": "^4.18.2", | ||||
|     "express-promise-router": "^4.1.1", | ||||
|     "express-query-boolean": "^2.0.0", | ||||
|     "express-session": "^1.18.0", | ||||
|     "floating-vue": "^5.2.2", | ||||
|     "knex": "^3.1.0", | ||||
|     "manticoresearch": "^4.0.0", | ||||
|     "markdown-it": "^14.0.0", | ||||
|  | @ -34,13 +40,15 @@ | |||
|     "path-to-regexp": "^6.2.1", | ||||
|     "pg": "^8.11.3", | ||||
|     "redis": "^4.6.12", | ||||
|     "sharp": "^0.32.6", | ||||
|     "sirv": "^2.0.3", | ||||
|     "vike": "^0.4.150", | ||||
|     "vite": "^4.5.1", | ||||
|     "vue": "^3.3.10", | ||||
|     "vue-virtual-scroller": "^2.0.0-beta.8", | ||||
|     "winston": "^3.11.0", | ||||
|     "winston-daily-rotate-file": "^4.7.1" | ||||
|     "winston-daily-rotate-file": "^4.7.1", | ||||
|     "yargs": "^17.7.2" | ||||
|   }, | ||||
|   "type": "module", | ||||
|   "devDependencies": { | ||||
|  |  | |||
|  | @ -0,0 +1,186 @@ | |||
| <template> | ||||
| 	<div class="login-container"> | ||||
| 		<div v-if="user"> | ||||
| 			You are already logged in as {{ user.username }}. | ||||
| 
 | ||||
| 			<ul> | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/user/${user.username}`" | ||||
| 						class="link" | ||||
| 					>View my profile</a> | ||||
| 				</li> | ||||
| 
 | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/updates`" | ||||
| 						class="link" | ||||
| 					>Check out latest porn updates</a> | ||||
| 				</li> | ||||
| 
 | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/actors`" | ||||
| 						class="link" | ||||
| 					>Browse the hottest porn stars</a> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<form | ||||
| 			v-else | ||||
| 			class="login-panel" | ||||
| 			@submit.prevent="login" | ||||
| 		> | ||||
| 			<div | ||||
| 				v-if="errorMsg" | ||||
| 				class="error" | ||||
| 			>{{ errorMsg }}</div> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="username" | ||||
| 				placeholder="Username or e-mail" | ||||
| 				class="input" | ||||
| 			> | ||||
| 
 | ||||
| 			<div class="password-container"> | ||||
| 				<input | ||||
| 					v-model="password" | ||||
| 					:type="showPassword ? 'input' : 'password'" | ||||
| 					placeholder="Password" | ||||
| 					class="password input" | ||||
| 				> | ||||
| 
 | ||||
| 				<div | ||||
| 					class="password-show" | ||||
| 					@click="showPassword = !showPassword" | ||||
| 				> | ||||
| 					<Icon | ||||
| 						v-show="!showPassword" | ||||
| 						icon="eye" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 
 | ||||
| 					<Icon | ||||
| 						v-show="showPassword" | ||||
| 						icon="eye-blocked" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<button class="button button-submit">Log in</button> | ||||
| 
 | ||||
| 			<a | ||||
| 				href="/signup" | ||||
| 				class="link" | ||||
| 			>Create an account</a> | ||||
| 		</form> | ||||
| 	</div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, inject } from 'vue'; | ||||
| 
 | ||||
| import { post } from '#/src/api.js'; | ||||
| import navigate from '#/src/navigate.js'; | ||||
| 
 | ||||
| const pageContext = inject('pageContext'); | ||||
| const user = pageContext.user; | ||||
| 
 | ||||
| const username = ref(''); | ||||
| const password = ref(''); | ||||
| 
 | ||||
| const errorMsg = ref(null); | ||||
| const showPassword = ref(false); | ||||
| 
 | ||||
| async function login() { | ||||
| 	errorMsg.value = null; | ||||
| 
 | ||||
| 	try { | ||||
| 		await post('/session', { | ||||
| 			username: username.value, | ||||
| 			password: password.value, | ||||
| 			redirect: pageContext.urlParsed.search.r, | ||||
| 		}); | ||||
| 
 | ||||
| 		navigate(decodeURIComponent(pageContext.urlParsed.search.r), null, { redirect: true }); | ||||
| 	} catch (error) { | ||||
| 		errorMsg.value = error.message; | ||||
| 	} | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .login-container { | ||||
| 	display: flex; | ||||
| 	flex-grow: 1; | ||||
| 	justify-content: center; | ||||
| 	align-items: center; | ||||
| 	background: var(--background-base-10); | ||||
| } | ||||
| 
 | ||||
| .login-panel { | ||||
| 	width: 20rem; | ||||
| 	max-width: 100%; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	gap: .5rem; | ||||
| 	padding: 1rem; | ||||
| 	margin: 1rem; | ||||
| 	border-radius: .5rem; | ||||
| 	box-shadow: 0 0 3px var(--shadow-weak-30); | ||||
| 	background: var(--background-base); | ||||
| 	font-size: 1rem; | ||||
| 
 | ||||
| 	.button { | ||||
| 		justify-content: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.link { | ||||
| 		margin-top: .5rem; | ||||
| 		text-align: center; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .password-container { | ||||
| 	display: flex; | ||||
| 	position: relative; | ||||
| } | ||||
| 
 | ||||
| .password { | ||||
| 	flex-grow: 1; | ||||
| } | ||||
| 
 | ||||
| .password-show { | ||||
| 	height: 100%; | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	position: absolute; | ||||
| 	right: 0; | ||||
| 	padding: 0 1rem 0 .5rem; | ||||
| 
 | ||||
| 	.icon { | ||||
| 		fill: var(--shadow); | ||||
| 	} | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		cursor: pointer; | ||||
| 
 | ||||
| 		.icon { | ||||
| 			fill: var(--primary); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .error { | ||||
| 	background: var(--error); | ||||
| 	color: var(--text-light); | ||||
| 	padding: .75rem 1rem; | ||||
| 	border-radius: .25rem; | ||||
| 	margin-bottom: .5rem; | ||||
| 	font-size: .9rem; | ||||
| 	font-weight: bold; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1 @@ | |||
| export default '/login'; | ||||
|  | @ -0,0 +1,43 @@ | |||
| <template> | ||||
| 	<div class="profile"> | ||||
| 		<div class="profile-header"> | ||||
| 			<img | ||||
| 				v-if="profile.avatar" | ||||
| 				:src="profile.avatar" | ||||
| 				class="avatar" | ||||
| 			> | ||||
| 
 | ||||
| 			<h2 class="username">{{ profile.username }}</h2> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { inject } from 'vue'; | ||||
| 
 | ||||
| const pageContext = inject('pageContext'); | ||||
| const profile = pageContext.pageProps.profile; | ||||
| 
 | ||||
| console.log('profile', profile); | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .profile-header { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: .5rem 1rem; | ||||
|     color: var(--highlight-strong-30); | ||||
|     background: var(--grey-dark-40); | ||||
| } | ||||
| 
 | ||||
| .username { | ||||
| 	margin: 0; | ||||
| } | ||||
| 
 | ||||
| .avatar { | ||||
| 	width: 2rem; | ||||
| 	height: 2rem; | ||||
| 	border-radius: .25rem; | ||||
| 	margin-right: 1rem; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,14 @@ | |||
| import { fetchUser } from '#/src/users.js'; | ||||
| 
 | ||||
| export async function onBeforeRender(pageContext) { | ||||
| 	const profile = await fetchUser(pageContext.routeParams.username); | ||||
| 
 | ||||
| 	return { | ||||
| 		pageContext: { | ||||
| 			title: profile.username, | ||||
| 			pageProps: { | ||||
| 				profile, // differentiate from authed 'user'
 | ||||
| 			}, | ||||
| 		}, | ||||
| 	}; | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| export default '/user/@username'; | ||||
|  | @ -1,3 +1,3 @@ | |||
| export default { | ||||
| 	passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env'], | ||||
| 	passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env', 'user'], | ||||
| }; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { createSSRApp, h } from 'vue'; | ||||
| import VueVirtualScroller from 'vue-virtual-scroller'; | ||||
| import FloatingVue from 'floating-vue'; | ||||
| 
 | ||||
| import { setPageContext } from './usePageContext.js'; | ||||
| 
 | ||||
|  | @ -31,6 +32,7 @@ function createApp(Page, pageProps, pageContext) { | |||
| 
 | ||||
| 	app.provide('pageContext', pageContext); | ||||
| 
 | ||||
| 	app.use(FloatingVue); | ||||
| 	app.use(VueVirtualScroller); | ||||
| 
 | ||||
| 	app.component('Link', Link); | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { parse } from '@brillout/json-serializer/parse'; | ||||
| import { parse } from '@brillout/json-serializer/parse'; /* eslint-disable-line import/extensions */ | ||||
| 
 | ||||
| const postHeaders = { | ||||
| 	mode: 'cors', | ||||
|  | @ -26,7 +26,7 @@ export async function get(path, query = {}) { | |||
| 		return body; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new Error(body.message); | ||||
| 	throw new Error(body.statusMessage); | ||||
| } | ||||
| 
 | ||||
| export async function post(path, data, { query } = {}) { | ||||
|  | @ -66,7 +66,7 @@ export async function patch(path, data, { query } = {}) { | |||
| 		return body; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new Error(body.message); | ||||
| 	throw new Error(body.statusMessage); | ||||
| } | ||||
| 
 | ||||
| export async function del(path, { data, query } = {}) { | ||||
|  | @ -86,5 +86,5 @@ export async function del(path, { data, query } = {}) { | |||
| 		return body; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new Error(body.message); | ||||
| 	throw new Error(body.statusMessage); | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| import yargs from 'yargs'; | ||||
| 
 | ||||
| const { argv } = yargs() | ||||
| 	.command('npm start') | ||||
| 	.option('debug', { | ||||
| 		describe: 'Show error stack traces', | ||||
| 		type: 'boolean', | ||||
| 		default: process.env.NODE_ENV === 'development', | ||||
| 	}); | ||||
| 
 | ||||
| export default argv; | ||||
|  | @ -0,0 +1,124 @@ | |||
| import config from 'config'; | ||||
| import util from 'util'; | ||||
| import crypto from 'crypto'; | ||||
| import fs from 'fs/promises'; | ||||
| import { createAvatar } from '@dicebear/core'; | ||||
| import { shapes } from '@dicebear/collection'; | ||||
| 
 | ||||
| import knex from './knex.js'; | ||||
| import { curateUser, fetchUser } from './users.js'; | ||||
| import { HttpError } from './errors.js'; | ||||
| 
 | ||||
| const scrypt = util.promisify(crypto.scrypt); | ||||
| 
 | ||||
| async function verifyPassword(password, storedPassword) { | ||||
| 	const [salt, hash] = storedPassword.split('/'); | ||||
| 	const hashedPassword = (await scrypt(password, salt, 64)).toString('hex'); | ||||
| 
 | ||||
| 	if (hashedPassword === hash) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new HttpError('Username or password incorrect', 401); | ||||
| } | ||||
| 
 | ||||
| async function generateAvatar(user) { | ||||
| 	const avatar = createAvatar(shapes, { | ||||
| 		seed: user.username, | ||||
| 		backgroundColor: ['f65596', '9b004b', '006b68', '5abab6'], | ||||
| 		shape1Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'], | ||||
| 		shape2Color: ['c162c6', '6074dd', '007dd2', '007ba9', '007170'], | ||||
| 		shape3Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'], | ||||
| 	}); | ||||
| 
 | ||||
| 	await fs.mkdir('media/avatars', { recursive: true }); | ||||
| 
 | ||||
| 	await avatar.png().toFile(`media/avatars/${user.id}_${user.username}.png`); | ||||
| } | ||||
| 
 | ||||
| export async function login(credentials) { | ||||
| 	if (!config.auth.login) { | ||||
| 		throw new HttpError('Authentication is disabled', 405); | ||||
| 	} | ||||
| 
 | ||||
| 	const user = await fetchUser(credentials.username.trim(), true); | ||||
| 
 | ||||
| 	if (!user) { | ||||
| 		throw new HttpError('Username or password incorrect', 401); | ||||
| 	} | ||||
| 
 | ||||
| 	await verifyPassword(credentials.password, user.password); | ||||
| 
 | ||||
| 	await knex('users') | ||||
| 		.update('last_login', 'NOW()') | ||||
| 		.where('id', user.id); | ||||
| 
 | ||||
| 	if (!user.avatar) { | ||||
| 		await generateAvatar(user); | ||||
| 	} | ||||
| 
 | ||||
| 	return curateUser(user); | ||||
| } | ||||
| 
 | ||||
| export async function signup(credentials) { | ||||
| 	if (!config.auth.signup) { | ||||
| 		throw new HttpError('Authentication is disabled', 405); | ||||
| 	} | ||||
| 
 | ||||
| 	const curatedUsername = credentials.username.trim(); | ||||
| 
 | ||||
| 	if (!curatedUsername) { | ||||
| 		throw new HttpError('Username required', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (curatedUsername.length < config.auth.usernameLength[0]) { | ||||
| 		throw new HttpError('Username is too short', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (curatedUsername.length > config.auth.usernameLength[1]) { | ||||
| 		throw new HttpError('Username is too long', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!config.auth.usernamePattern.test(curatedUsername)) { | ||||
| 		throw new HttpError('Username contains invalid characters', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!credentials.email) { | ||||
| 		throw new HttpError('E-mail required', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (credentials.password?.length < 3) { | ||||
| 		throw new HttpError('Password must be 3 characters or longer', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	const existingUser = await knex('users') | ||||
| 		.where('username', curatedUsername) | ||||
| 		.orWhere('email', credentials.email) | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (existingUser) { | ||||
| 		throw new HttpError('Username or e-mail already in use', 409); | ||||
| 	} | ||||
| 
 | ||||
| 	const salt = crypto.randomBytes(16).toString('hex'); | ||||
| 	const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex'); | ||||
| 	const storedPassword = `${salt}/${hashedPassword}`; | ||||
| 
 | ||||
| 	const [{ id: userId }] = await knex('users') | ||||
| 		.insert({ | ||||
| 			username: curatedUsername, | ||||
| 			email: credentials.email, | ||||
| 			password: storedPassword, | ||||
| 		}) | ||||
| 		.returning('id'); | ||||
| 
 | ||||
| 	await knex('stashes').insert({ | ||||
| 		user_id: userId, | ||||
| 		name: 'Favorites', | ||||
| 		slug: 'favorites', | ||||
| 		public: false, | ||||
| 		primary: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	return fetchUser(userId); | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| import knex from './knex.js'; | ||||
| // import { curateStash } from './stashes.js';
 | ||||
| 
 | ||||
| export function curateUser(user) { | ||||
| 	if (!user) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	const ability = [...(user.role_abilities || []), ...(user.abilities || [])]; | ||||
| 
 | ||||
| 	const curatedUser = { | ||||
| 		id: user.id, | ||||
| 		username: user.username, | ||||
| 		email: user.email, | ||||
| 		emailVerified: user.email_verified, | ||||
| 		identityVerified: user.identity_verified, | ||||
| 		ability, | ||||
| 		avatar: `/media/avatars/${user.id}_${user.username}.png`, | ||||
| 		createdAt: user.created_at, | ||||
| 		// stashes: user.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [],
 | ||||
| 	}; | ||||
| 
 | ||||
| 	return curatedUser; | ||||
| } | ||||
| 
 | ||||
| export async function fetchUser(userId, raw) { | ||||
| 	const user = await knex('users') | ||||
| 		.select(knex.raw('users.*, users_roles.abilities as role_abilities, COALESCE(json_agg(stashes ORDER BY stashes.created_at) FILTER (WHERE stashes.id IS NOT NULL), \'[]\') as stashes')) | ||||
| 		.modify((builder) => { | ||||
| 			if (typeof userId === 'number') { | ||||
| 				builder.where('users.id', userId); | ||||
| 			} | ||||
| 
 | ||||
| 			if (typeof userId === 'string') { | ||||
| 				builder | ||||
| 					.where('users.username', userId) | ||||
| 					.orWhere('users.email', userId); | ||||
| 			} | ||||
| 		}) | ||||
| 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | ||||
| 		.leftJoin('stashes', 'stashes.user_id', 'users.id') | ||||
| 		.groupBy('users.id', 'users_roles.role') | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (raw) { | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
| 	return curateUser(user); | ||||
| } | ||||
|  | @ -0,0 +1,49 @@ | |||
| /* eslint-disable no-param-reassign */ | ||||
| import { login, signup } from '../auth.js'; | ||||
| import { fetchUser } from '../users.js'; | ||||
| 
 | ||||
| export async function setUserApi(req, res, next) { | ||||
| 	if (req.session.user) { | ||||
| 		req.user = req.session.user; | ||||
| 	} | ||||
| 
 | ||||
| 	next(); | ||||
| } | ||||
| 
 | ||||
| export async function loginApi(req, res) { | ||||
| 	console.log('login!', req.body); | ||||
| 
 | ||||
| 	const user = await login(req.body); | ||||
| 
 | ||||
| 	req.session.user = user; | ||||
| 	res.send(user); | ||||
| } | ||||
| 
 | ||||
| export async function logoutApi(req, res) { | ||||
| 	req.session.destroy((error) => { | ||||
| 		if (error) { | ||||
| 			res.status(500).send(); | ||||
| 		} | ||||
| 
 | ||||
| 		res.status(204).send(); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export async function fetchMeApi(req, res) { | ||||
| 	if (req.session.user) { | ||||
| 		req.session.user = await fetchUser(req.session.user.id, false, req.session.user); | ||||
| 
 | ||||
| 		res.send(req.session.user); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	res.status(401).send(); | ||||
| } | ||||
| 
 | ||||
| export async function signupApi(req, res) { | ||||
| 	const user = await signup(req.body); | ||||
| 
 | ||||
| 	req.session.user = user; | ||||
| 	res.send(user); | ||||
| } | ||||
| /* eslint-enable no-param-reassign */ | ||||
|  | @ -0,0 +1,26 @@ | |||
| import argv from '../argv.js'; | ||||
| import initLogger from '../logger.js'; | ||||
| 
 | ||||
| const logger = initLogger(); | ||||
| 
 | ||||
| export default function errorHandler(error, req, res, _next) { | ||||
| 	logger.warn(`Failed to fulfill request to ${req.path} (${error.httpCode || 500}): ${error.message}`); | ||||
| 
 | ||||
| 	if (argv.debug) { | ||||
| 		logger.error(error); | ||||
| 	} | ||||
| 
 | ||||
| 	if (error.httpCode) { | ||||
| 		res.status(error.httpCode).send({ | ||||
| 			statusCode: error.httpCode, | ||||
| 			statusMessage: error.message, | ||||
| 		}); | ||||
| 
 | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	res.status(500).send({ | ||||
| 		statusCode: 500, | ||||
| 		statusMessage: 'Oops... our server messed up. We will be investigating this incident, our apologies for the inconvenience.', | ||||
| 	}); | ||||
| } | ||||
|  | @ -15,15 +15,27 @@ import config from 'config'; | |||
| import express from 'express'; | ||||
| import boolParser from 'express-query-boolean'; | ||||
| import Router from 'express-promise-router'; | ||||
| import session from 'express-session'; | ||||
| import RedisStore from 'connect-redis'; | ||||
| import compression from 'compression'; | ||||
| import { renderPage } from 'vike/server'; // eslint-disable-line import/extensions
 | ||||
| 
 | ||||
| // import root from './root.js';
 | ||||
| 
 | ||||
| import redis from '../redis.js'; | ||||
| 
 | ||||
| import errorHandler from './error.js'; | ||||
| 
 | ||||
| import { fetchScenesApi } from './scenes.js'; | ||||
| import { fetchActorsApi } from './actors.js'; | ||||
| import { fetchMoviesApi } from './movies.js'; | ||||
| 
 | ||||
| import { | ||||
| 	setUserApi, | ||||
| 	loginApi, | ||||
| 	logoutApi, | ||||
| } from './auth.js'; | ||||
| 
 | ||||
| import initLogger from '../logger.js'; | ||||
| 
 | ||||
| const logger = initLogger(); | ||||
|  | @ -42,6 +54,20 @@ export default async function initServer() { | |||
| 	router.use('/', express.static('static')); | ||||
| 	router.use('/media', express.static(config.media.path)); | ||||
| 
 | ||||
| 	router.use(express.json()); | ||||
| 
 | ||||
| 	const redisStore = new RedisStore({ | ||||
| 		client: redis, | ||||
| 		prefix: 'traxxx:session:', | ||||
| 	}); | ||||
| 
 | ||||
| 	router.use(session({ | ||||
| 		...config.web.session, | ||||
| 		store: redisStore, | ||||
| 	})); | ||||
| 
 | ||||
| 	router.use(setUserApi); | ||||
| 
 | ||||
| 	// Vite integration
 | ||||
| 	if (isProduction) { | ||||
| 		// In production, we need to serve our static assets ourselves.
 | ||||
|  | @ -69,6 +95,9 @@ export default async function initServer() { | |||
| 
 | ||||
| 	router.get('/api/movies', fetchMoviesApi); | ||||
| 
 | ||||
| 	router.post('/api/session', loginApi); | ||||
| 	router.delete('/api/session', logoutApi); | ||||
| 
 | ||||
| 	// ...
 | ||||
| 	// Other middlewares (e.g. some RPC middleware such as Telefunc)
 | ||||
| 	// ...
 | ||||
|  | @ -79,6 +108,7 @@ export default async function initServer() { | |||
| 		const pageContextInit = { | ||||
| 			urlOriginal: req.originalUrl, | ||||
| 			urlQuery: req.query, // vike's own query does not apply boolean parser
 | ||||
| 			user: req.user, | ||||
| 			env: { | ||||
| 				maxAggregateSize: config.database.manticore.maxAggregateSize, | ||||
| 			}, | ||||
|  | @ -110,6 +140,7 @@ export default async function initServer() { | |||
| 		res.send(body); | ||||
| 	}); | ||||
| 
 | ||||
| 	router.use(errorHandler); | ||||
| 	app.use(router); | ||||
| 
 | ||||
| 	const port = process.env.PORT || config.web.port || 3000; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue