⏪️ 重现旧 UI #4
							
								
								
									
										59
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					<component name="ProjectCodeStyleConfiguration">
 | 
				
			||||||
 | 
					  <code_scheme name="Project" version="173">
 | 
				
			||||||
 | 
					    <HTMLCodeStyleSettings>
 | 
				
			||||||
 | 
					      <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
 | 
				
			||||||
 | 
					    </HTMLCodeStyleSettings>
 | 
				
			||||||
 | 
					    <JSCodeStyleSettings version="0">
 | 
				
			||||||
 | 
					      <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
 | 
				
			||||||
 | 
					      <option name="FORCE_SEMICOLON_STYLE" value="true" />
 | 
				
			||||||
 | 
					      <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
 | 
				
			||||||
 | 
					      <option name="FORCE_QUOTE_STYlE" value="true" />
 | 
				
			||||||
 | 
					      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
 | 
				
			||||||
 | 
					      <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
 | 
				
			||||||
 | 
					      <option name="SPACES_WITHIN_IMPORTS" value="true" />
 | 
				
			||||||
 | 
					    </JSCodeStyleSettings>
 | 
				
			||||||
 | 
					    <TypeScriptCodeStyleSettings version="0">
 | 
				
			||||||
 | 
					      <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
 | 
				
			||||||
 | 
					      <option name="FORCE_SEMICOLON_STYLE" value="true" />
 | 
				
			||||||
 | 
					      <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
 | 
				
			||||||
 | 
					      <option name="FORCE_QUOTE_STYlE" value="true" />
 | 
				
			||||||
 | 
					      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
 | 
				
			||||||
 | 
					      <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
 | 
				
			||||||
 | 
					      <option name="SPACES_WITHIN_IMPORTS" value="true" />
 | 
				
			||||||
 | 
					    </TypeScriptCodeStyleSettings>
 | 
				
			||||||
 | 
					    <VueCodeStyleSettings>
 | 
				
			||||||
 | 
					      <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
 | 
				
			||||||
 | 
					      <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
 | 
				
			||||||
 | 
					    </VueCodeStyleSettings>
 | 
				
			||||||
 | 
					    <codeStyleSettings language="HTML">
 | 
				
			||||||
 | 
					      <option name="SOFT_MARGINS" value="120" />
 | 
				
			||||||
 | 
					      <indentOptions>
 | 
				
			||||||
 | 
					        <option name="INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="CONTINUATION_INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="TAB_SIZE" value="2" />
 | 
				
			||||||
 | 
					      </indentOptions>
 | 
				
			||||||
 | 
					    </codeStyleSettings>
 | 
				
			||||||
 | 
					    <codeStyleSettings language="JavaScript">
 | 
				
			||||||
 | 
					      <option name="SOFT_MARGINS" value="120" />
 | 
				
			||||||
 | 
					      <indentOptions>
 | 
				
			||||||
 | 
					        <option name="INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="CONTINUATION_INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="TAB_SIZE" value="2" />
 | 
				
			||||||
 | 
					      </indentOptions>
 | 
				
			||||||
 | 
					    </codeStyleSettings>
 | 
				
			||||||
 | 
					    <codeStyleSettings language="TypeScript">
 | 
				
			||||||
 | 
					      <option name="SOFT_MARGINS" value="120" />
 | 
				
			||||||
 | 
					      <indentOptions>
 | 
				
			||||||
 | 
					        <option name="INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="CONTINUATION_INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					        <option name="TAB_SIZE" value="2" />
 | 
				
			||||||
 | 
					      </indentOptions>
 | 
				
			||||||
 | 
					    </codeStyleSettings>
 | 
				
			||||||
 | 
					    <codeStyleSettings language="Vue">
 | 
				
			||||||
 | 
					      <option name="SOFT_MARGINS" value="120" />
 | 
				
			||||||
 | 
					      <indentOptions>
 | 
				
			||||||
 | 
					        <option name="CONTINUATION_INDENT_SIZE" value="2" />
 | 
				
			||||||
 | 
					      </indentOptions>
 | 
				
			||||||
 | 
					    </codeStyleSettings>
 | 
				
			||||||
 | 
					  </code_scheme>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
							
								
								
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<component name="ProjectCodeStyleConfiguration">
 | 
				
			||||||
 | 
					  <state>
 | 
				
			||||||
 | 
					    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
 | 
				
			||||||
 | 
					  </state>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
@@ -5024,6 +5024,7 @@ true posixrules
 | 
				
			|||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
    <table id="292" parent="267" name="passport_notifications">
 | 
					    <table id="292" parent="267" name="passport_notifications">
 | 
				
			||||||
      <ObjectId>16445</ObjectId>
 | 
					      <ObjectId>16445</ObjectId>
 | 
				
			||||||
 | 
					      <Outdated>1</Outdated>
 | 
				
			||||||
      <StateNumber>25751</StateNumber>
 | 
					      <StateNumber>25751</StateNumber>
 | 
				
			||||||
      <AccessMethodId>2</AccessMethodId>
 | 
					      <AccessMethodId>2</AccessMethodId>
 | 
				
			||||||
      <OwnerName>postgres</OwnerName>
 | 
					      <OwnerName>postgres</OwnerName>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										79
									
								
								.idea/workspace.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										79
									
								
								.idea/workspace.xml
									
									
									
										generated
									
									
									
								
							@@ -4,9 +4,13 @@
 | 
				
			|||||||
    <option name="autoReloadType" value="ALL" />
 | 
					    <option name="autoReloadType" value="ALL" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
  <component name="ChangeListManager">
 | 
					  <component name="ChangeListManager">
 | 
				
			||||||
    <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":ambulance: Fix query services too much 429">
 | 
					    <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":arrow_up: Fix notification listen">
 | 
				
			||||||
      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
 | 
					      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
 | 
				
			||||||
      <change beforePath="$PROJECT_DIR$/pkg/hyper/conn.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/hyper/conn.go" afterDir="false" />
 | 
					      <change beforePath="$PROJECT_DIR$/pkg/internal/models/tokens.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/models/tokens.go" afterDir="false" />
 | 
				
			||||||
 | 
					      <change beforePath="$PROJECT_DIR$/pkg/internal/server/api/auth_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/auth_api.go" afterDir="false" />
 | 
				
			||||||
 | 
					      <change beforePath="$PROJECT_DIR$/pkg/internal/services/accounts.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/services/accounts.go" afterDir="false" />
 | 
				
			||||||
 | 
					      <change beforePath="$PROJECT_DIR$/pkg/internal/services/tokens.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/services/tokens.go" afterDir="false" />
 | 
				
			||||||
 | 
					      <change beforePath="$PROJECT_DIR$/pkg/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/main.go" afterDir="false" />
 | 
				
			||||||
    </list>
 | 
					    </list>
 | 
				
			||||||
    <option name="SHOW_DIALOG" value="false" />
 | 
					    <option name="SHOW_DIALOG" value="false" />
 | 
				
			||||||
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
 | 
					    <option name="HIGHLIGHT_CONFLICTS" value="true" />
 | 
				
			||||||
@@ -41,40 +45,41 @@
 | 
				
			|||||||
    <option name="hideEmptyMiddlePackages" value="true" />
 | 
					    <option name="hideEmptyMiddlePackages" value="true" />
 | 
				
			||||||
    <option name="showLibraryContents" value="true" />
 | 
					    <option name="showLibraryContents" value="true" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
  <component name="PropertiesComponent">{
 | 
					  <component name="PropertiesComponent"><![CDATA[{
 | 
				
			||||||
  "keyToString": {
 | 
					  "keyToString": {
 | 
				
			||||||
    "DefaultGoTemplateProperty": "Go File",
 | 
					    "DefaultGoTemplateProperty": "Go File",
 | 
				
			||||||
    "Go Build.Backend.executor": "Run",
 | 
					    "Go Build.Backend.executor": "Run",
 | 
				
			||||||
    "Go 构建.Backend.executor": "Run",
 | 
					    "Go 构建.Backend.executor": "Run",
 | 
				
			||||||
    "RunOnceActivity.ShowReadmeOnStart": "true",
 | 
					    "RunOnceActivity.ShowReadmeOnStart": "true",
 | 
				
			||||||
    "RunOnceActivity.go.formatter.settings.were.checked": "true",
 | 
					    "RunOnceActivity.go.formatter.settings.were.checked": "true",
 | 
				
			||||||
    "RunOnceActivity.go.migrated.go.modules.settings": "true",
 | 
					    "RunOnceActivity.go.migrated.go.modules.settings": "true",
 | 
				
			||||||
    "RunOnceActivity.go.modules.automatic.dependencies.download": "true",
 | 
					    "RunOnceActivity.go.modules.automatic.dependencies.download": "true",
 | 
				
			||||||
    "RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
 | 
					    "RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
 | 
				
			||||||
    "git-widget-placeholder": "master",
 | 
					    "git-widget-placeholder": "refactor/v2",
 | 
				
			||||||
    "go.import.settings.migrated": "true",
 | 
					    "go.import.settings.migrated": "true",
 | 
				
			||||||
    "go.sdk.automatically.set": "true",
 | 
					    "go.sdk.automatically.set": "true",
 | 
				
			||||||
    "last_opened_file_path": "/Users/littlesheep",
 | 
					    "last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/pkg/internal/server/api",
 | 
				
			||||||
    "node.js.detected.package.eslint": "true",
 | 
					    "node.js.detected.package.eslint": "true",
 | 
				
			||||||
    "node.js.selected.package.eslint": "(autodetect)",
 | 
					    "node.js.selected.package.eslint": "(autodetect)",
 | 
				
			||||||
    "nodejs_package_manager_path": "npm",
 | 
					    "nodejs_package_manager_path": "npm",
 | 
				
			||||||
    "run.code.analysis.last.selected.profile": "pProject Default",
 | 
					    "run.code.analysis.last.selected.profile": "pProject Default",
 | 
				
			||||||
    "settings.editor.selected.configurable": "preferences.pluginManager",
 | 
					    "settings.editor.selected.configurable": "preferences.pluginManager",
 | 
				
			||||||
    "vue.rearranger.settings.migration": "true"
 | 
					    "ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
 | 
				
			||||||
 | 
					    "vue.rearranger.settings.migration": "true"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "keyToStringList": {
 | 
					  "keyToStringList": {
 | 
				
			||||||
    "DatabaseDriversLRU": [
 | 
					    "DatabaseDriversLRU": [
 | 
				
			||||||
      "postgresql"
 | 
					      "postgresql"
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}</component>
 | 
					}]]></component>
 | 
				
			||||||
  <component name="RecentsManager">
 | 
					  <component name="RecentsManager">
 | 
				
			||||||
    <key name="CopyFile.RECENT_KEYS">
 | 
					    <key name="CopyFile.RECENT_KEYS">
 | 
				
			||||||
 | 
					      <recent name="$PROJECT_DIR$/pkg/internal/server/api" />
 | 
				
			||||||
 | 
					      <recent name="$PROJECT_DIR$/web" />
 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg/services" />
 | 
					      <recent name="$PROJECT_DIR$/pkg/services" />
 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg/server/ui" />
 | 
					      <recent name="$PROJECT_DIR$/pkg/server/ui" />
 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg/views/users" />
 | 
					      <recent name="$PROJECT_DIR$/pkg/views/users" />
 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg/views" />
 | 
					 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg" />
 | 
					 | 
				
			||||||
    </key>
 | 
					    </key>
 | 
				
			||||||
    <key name="MoveFile.RECENT_KEYS">
 | 
					    <key name="MoveFile.RECENT_KEYS">
 | 
				
			||||||
      <recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
 | 
					      <recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
 | 
				
			||||||
@@ -130,7 +135,7 @@
 | 
				
			|||||||
                  <entry key="branch">
 | 
					                  <entry key="branch">
 | 
				
			||||||
                    <value>
 | 
					                    <value>
 | 
				
			||||||
                      <list>
 | 
					                      <list>
 | 
				
			||||||
                        <option value="master" />
 | 
					                        <option value="refactor/v2" />
 | 
				
			||||||
                      </list>
 | 
					                      </list>
 | 
				
			||||||
                    </value>
 | 
					                    </value>
 | 
				
			||||||
                  </entry>
 | 
					                  </entry>
 | 
				
			||||||
@@ -143,14 +148,7 @@
 | 
				
			|||||||
    </option>
 | 
					    </option>
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
  <component name="VcsManagerConfiguration">
 | 
					  <component name="VcsManagerConfiguration">
 | 
				
			||||||
    <MESSAGE value=":zap: In memory auth context cache" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":sparkles: Bug fixes of permission check" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":sparkles: Check permissions GRPC method" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":recycle: Use paperclip to store avatar and more" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":bug: Bug fixes in update avatar" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":sparkles: Firebase is back" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":sparkles: Apple push notification services" />
 | 
					    <MESSAGE value=":sparkles: Apple push notification services" />
 | 
				
			||||||
    <MESSAGE value=":bug: Bug fixes" />
 | 
					 | 
				
			||||||
    <MESSAGE value=":bug: Bug fix and fix" />
 | 
					    <MESSAGE value=":bug: Bug fix and fix" />
 | 
				
			||||||
    <MESSAGE value=":bug: Fix APNs non-production" />
 | 
					    <MESSAGE value=":bug: Fix APNs non-production" />
 | 
				
			||||||
    <MESSAGE value=":bug: Bug fixes on notification badges for APNs" />
 | 
					    <MESSAGE value=":bug: Bug fixes on notification badges for APNs" />
 | 
				
			||||||
@@ -168,7 +166,14 @@
 | 
				
			|||||||
    <MESSAGE value=":bug: Fix registration service issue" />
 | 
					    <MESSAGE value=":bug: Fix registration service issue" />
 | 
				
			||||||
    <MESSAGE value=":bug: Fix avatar url missing endpoint prefix" />
 | 
					    <MESSAGE value=":bug: Fix avatar url missing endpoint prefix" />
 | 
				
			||||||
    <MESSAGE value=":ambulance: Fix query services too much 429" />
 | 
					    <MESSAGE value=":ambulance: Fix query services too much 429" />
 | 
				
			||||||
    <option name="LAST_COMMIT_MESSAGE" value=":ambulance: Fix query services too much 429" />
 | 
					    <MESSAGE value=":ambulance: Fix nil map panic" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":recycle: Update the sign in web page to the latest API" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":wastebasket: Remove the personal page" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":recycle: OAuth authenticate" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":sparkles: Recommend app component" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":bug: Bug fixes" />
 | 
				
			||||||
 | 
					    <MESSAGE value=":arrow_up: Fix notification listen" />
 | 
				
			||||||
 | 
					    <option name="LAST_COMMIT_MESSAGE" value=":arrow_up: Fix notification listen" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
  <component name="VgoProject">
 | 
					  <component name="VgoProject">
 | 
				
			||||||
    <settings-migrated>true</settings-migrated>
 | 
					    <settings-migrated>true</settings-migrated>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@@ -10,9 +10,7 @@ require (
 | 
				
			|||||||
	github.com/go-playground/validator/v10 v10.17.0
 | 
						github.com/go-playground/validator/v10 v10.17.0
 | 
				
			||||||
	github.com/gofiber/contrib/websocket v1.3.0
 | 
						github.com/gofiber/contrib/websocket v1.3.0
 | 
				
			||||||
	github.com/gofiber/fiber/v2 v2.52.4
 | 
						github.com/gofiber/fiber/v2 v2.52.4
 | 
				
			||||||
	github.com/gofiber/template/html/v2 v2.1.1
 | 
					 | 
				
			||||||
	github.com/golang-jwt/jwt/v5 v5.2.0
 | 
						github.com/golang-jwt/jwt/v5 v5.2.0
 | 
				
			||||||
	github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
 | 
					 | 
				
			||||||
	github.com/google/uuid v1.6.0
 | 
						github.com/google/uuid v1.6.0
 | 
				
			||||||
	github.com/hashicorp/consul/api v1.29.1
 | 
						github.com/hashicorp/consul/api v1.29.1
 | 
				
			||||||
	github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
 | 
						github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
 | 
				
			||||||
@@ -57,8 +55,6 @@ require (
 | 
				
			|||||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
						github.com/go-playground/locales v0.14.1 // indirect
 | 
				
			||||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
						github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
				
			||||||
	github.com/go-sql-driver/mysql v1.7.1 // indirect
 | 
						github.com/go-sql-driver/mysql v1.7.1 // indirect
 | 
				
			||||||
	github.com/gofiber/template v1.8.3 // indirect
 | 
					 | 
				
			||||||
	github.com/gofiber/utils v1.1.0 // indirect
 | 
					 | 
				
			||||||
	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 | 
						github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 | 
				
			||||||
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 | 
						github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 | 
				
			||||||
	github.com/golang/protobuf v1.5.4 // indirect
 | 
						github.com/golang/protobuf v1.5.4 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							@@ -100,12 +100,6 @@ github.com/gofiber/contrib/websocket v1.3.0/go.mod h1:xguaOzn2ZZ759LavtosEP+rcxI
 | 
				
			|||||||
github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ=
 | 
					github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ=
 | 
				
			||||||
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
 | 
					github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
 | 
				
			||||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
 | 
					github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
 | 
				
			||||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
 | 
					 | 
				
			||||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
 | 
					 | 
				
			||||||
github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8=
 | 
					 | 
				
			||||||
github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
 | 
					 | 
				
			||||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
 | 
					 | 
				
			||||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
 | 
					 | 
				
			||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 | 
					github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 | 
				
			||||||
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 | 
					github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 | 
				
			||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 | 
					github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 | 
				
			||||||
@@ -135,8 +129,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 | 
				
			|||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
					github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
				
			||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 | 
					github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 | 
				
			||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 | 
					github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 | 
				
			||||||
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM=
 | 
					 | 
				
			||||||
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
 | 
					 | 
				
			||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
					github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
				
			||||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
 | 
					github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
 | 
				
			||||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 | 
					github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@ var AutoMaintainRange = []any{
 | 
				
			|||||||
	&models.Account{},
 | 
						&models.Account{},
 | 
				
			||||||
	&models.AuthFactor{},
 | 
						&models.AuthFactor{},
 | 
				
			||||||
	&models.AccountProfile{},
 | 
						&models.AccountProfile{},
 | 
				
			||||||
	&models.AccountPage{},
 | 
					 | 
				
			||||||
	&models.AccountContact{},
 | 
						&models.AccountContact{},
 | 
				
			||||||
	&models.AccountFriendship{},
 | 
						&models.AccountFriendship{},
 | 
				
			||||||
	&models.Badge{},
 | 
						&models.Badge{},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
package pkg
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "embed"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//go:embed all:views/*
 | 
					 | 
				
			||||||
var FS embed.FS
 | 
					 | 
				
			||||||
@@ -17,27 +17,31 @@ type Server struct {
 | 
				
			|||||||
	proto.UnimplementedFriendshipsServer
 | 
						proto.UnimplementedFriendshipsServer
 | 
				
			||||||
	proto.UnimplementedRealmsServer
 | 
						proto.UnimplementedRealmsServer
 | 
				
			||||||
	health.UnimplementedHealthServer
 | 
						health.UnimplementedHealthServer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						srv *grpc.Server
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var S *grpc.Server
 | 
					func NewServer() *Server {
 | 
				
			||||||
 | 
						server := &Server{
 | 
				
			||||||
 | 
							srv: grpc.NewServer(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewGRPC() {
 | 
						proto.RegisterAuthServer(server.srv, &Server{})
 | 
				
			||||||
	S = grpc.NewServer()
 | 
						proto.RegisterNotifyServer(server.srv, &Server{})
 | 
				
			||||||
 | 
						proto.RegisterFriendshipsServer(server.srv, &Server{})
 | 
				
			||||||
 | 
						proto.RegisterRealmsServer(server.srv, &Server{})
 | 
				
			||||||
 | 
						health.RegisterHealthServer(server.srv, &Server{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proto.RegisterAuthServer(S, &Server{})
 | 
						reflection.Register(server.srv)
 | 
				
			||||||
	proto.RegisterNotifyServer(S, &Server{})
 | 
					 | 
				
			||||||
	proto.RegisterFriendshipsServer(S, &Server{})
 | 
					 | 
				
			||||||
	proto.RegisterRealmsServer(S, &Server{})
 | 
					 | 
				
			||||||
	health.RegisterHealthServer(S, &Server{})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	reflection.Register(S)
 | 
						return server
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ListenGRPC() error {
 | 
					func (v *Server) Listen() error {
 | 
				
			||||||
	listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
 | 
						listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return S.Serve(listener)
 | 
						return v.srv.Serve(listener)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,6 @@ type Account struct {
 | 
				
			|||||||
	PermNodes   datatypes.JSONMap `json:"perm_nodes"`
 | 
						PermNodes   datatypes.JSONMap `json:"perm_nodes"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Profile         AccountProfile   `json:"profile"`
 | 
						Profile         AccountProfile   `json:"profile"`
 | 
				
			||||||
	PersonalPage    AccountPage      `json:"personal_page"`
 | 
					 | 
				
			||||||
	Badges          []Badge          `json:"badges"`
 | 
						Badges          []Badge          `json:"badges"`
 | 
				
			||||||
	Contacts        []AccountContact `json:"contacts"`
 | 
						Contacts        []AccountContact `json:"contacts"`
 | 
				
			||||||
	RealmIdentities []RealmMember    `json:"realm_identities"`
 | 
						RealmIdentities []RealmMember    `json:"realm_identities"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
package models
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"gorm.io/datatypes"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,18 +13,3 @@ type AccountProfile struct {
 | 
				
			|||||||
	Birthday   *time.Time `json:"birthday"`
 | 
						Birthday   *time.Time `json:"birthday"`
 | 
				
			||||||
	AccountID  uint       `json:"account_id"`
 | 
						AccountID  uint       `json:"account_id"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
type AccountPage struct {
 | 
					 | 
				
			||||||
	BaseModel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Content   string                                `json:"content"`
 | 
					 | 
				
			||||||
	Script    string                                `json:"script"`
 | 
					 | 
				
			||||||
	Style     string                                `json:"style"`
 | 
					 | 
				
			||||||
	Links     datatypes.JSONSlice[AccountPageLinks] `json:"links"`
 | 
					 | 
				
			||||||
	AccountID uint                                  `json:"account_id"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type AccountPageLinks struct {
 | 
					 | 
				
			||||||
	Label string `json:"label"`
 | 
					 | 
				
			||||||
	Url   string `json:"url"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,6 @@ type MagicToken struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	Code      string     `json:"code"`
 | 
						Code      string     `json:"code"`
 | 
				
			||||||
	Type      int8       `json:"type"`
 | 
						Type      int8       `json:"type"`
 | 
				
			||||||
	AssignTo  *uint      `json:"assign_to"`
 | 
						AccountID *uint      `json:"account_id"`
 | 
				
			||||||
	ExpiredAt *time.Time `json:"expired_at"`
 | 
						ExpiredAt *time.Time `json:"expired_at"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -123,7 +123,7 @@ func editUserinfo(c *fiber.Ctx) error {
 | 
				
			|||||||
	return c.SendStatus(fiber.StatusOK)
 | 
						return c.SendStatus(fiber.StatusOK)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func killSession(c *fiber.Ctx) error {
 | 
					func killTicket(c *fiber.Ctx) error {
 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
						if err := exts.EnsureAuthenticated(c); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,8 @@ func doAuthenticate(c *fiber.Ctx) error {
 | 
				
			|||||||
	user, err := services.LookupAccount(data.Username)
 | 
						user, err := services.LookupAccount(data.Username)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
 | 
							return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
 | 
				
			||||||
 | 
						} else if user.ConfirmedAt == nil {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusForbidden, "account was not confirmed")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
						ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
				
			||||||
@@ -36,7 +38,7 @@ func doAuthenticate(c *fiber.Ctx) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return c.JSON(fiber.Map{
 | 
						return c.JSON(fiber.Map{
 | 
				
			||||||
		"is_finished": ticket.IsAvailable(),
 | 
							"is_finished": ticket.IsAvailable() == nil,
 | 
				
			||||||
		"ticket":      ticket,
 | 
							"ticket":      ticket,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -68,7 +70,7 @@ func doMultiFactorAuthenticate(c *fiber.Ctx) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return c.JSON(fiber.Map{
 | 
						return c.JSON(fiber.Map{
 | 
				
			||||||
		"is_finished": ticket.IsAvailable(),
 | 
							"is_finished": ticket.IsAvailable() == nil,
 | 
				
			||||||
		"ticket":      ticket,
 | 
							"ticket":      ticket,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,29 @@
 | 
				
			|||||||
package api
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
						"github.com/gofiber/fiber/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getAvailableFactors(c *fiber.Ctx) error {
 | 
				
			||||||
 | 
						ticketId := c.QueryInt("ticketId", 0)
 | 
				
			||||||
 | 
						if ticketId <= 0 {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, "must provide ticket id as a query parameter")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ticket, err := services.GetTicket(uint(ticketId))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						factors, err := services.ListUserFactor(ticket.AccountID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return c.JSON(factors)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func requestFactorToken(c *fiber.Ctx) error {
 | 
					func requestFactorToken(c *fiber.Ctx) error {
 | 
				
			||||||
	id, _ := c.ParamsInt("factorId", 0)
 | 
						id, _ := c.ParamsInt("factorId", 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,12 +26,10 @@ func MapAPIs(app *fiber.App) {
 | 
				
			|||||||
			me.Put("/banner", setBanner)
 | 
								me.Put("/banner", setBanner)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			me.Get("/", getUserinfo)
 | 
								me.Get("/", getUserinfo)
 | 
				
			||||||
			me.Get("/page", getOwnPersonalPage)
 | 
					 | 
				
			||||||
			me.Put("/", editUserinfo)
 | 
								me.Put("/", editUserinfo)
 | 
				
			||||||
			me.Put("/page", editPersonalPage)
 | 
					 | 
				
			||||||
			me.Get("/events", getEvents)
 | 
								me.Get("/events", getEvents)
 | 
				
			||||||
			me.Get("/tickets", getTickets)
 | 
								me.Get("/tickets", getTickets)
 | 
				
			||||||
			me.Delete("/tickets/:ticketId", killSession)
 | 
								me.Delete("/tickets/:ticketId", killTicket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			me.Post("/confirm", doRegisterConfirm)
 | 
								me.Post("/confirm", doRegisterConfirm)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,14 +47,22 @@ func MapAPIs(app *fiber.App) {
 | 
				
			|||||||
		directory := api.Group("/users/:alias").Name("User Directory")
 | 
							directory := api.Group("/users/:alias").Name("User Directory")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			directory.Get("/", getOtherUserinfo)
 | 
								directory.Get("/", getOtherUserinfo)
 | 
				
			||||||
			directory.Get("/page", getPersonalPage)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		api.Post("/users", doRegister)
 | 
							api.Post("/users", doRegister)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		api.Post("/auth", doAuthenticate)
 | 
							auth := api.Group("/auth").Name("Auth")
 | 
				
			||||||
		api.Post("/auth/token", getToken)
 | 
							{
 | 
				
			||||||
		api.Post("/auth/factors/:factorId", requestFactorToken)
 | 
								auth.Post("/", doAuthenticate)
 | 
				
			||||||
 | 
								auth.Post("/mfa", doMultiFactorAuthenticate)
 | 
				
			||||||
 | 
								auth.Post("/token", getToken)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auth.Get("/factors", getAvailableFactors)
 | 
				
			||||||
 | 
								auth.Post("/factors/:factorId", requestFactorToken)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auth.Get("/o/authorize", tryAuthorizeThirdClient)
 | 
				
			||||||
 | 
								auth.Post("/o/authorize", authorizeThirdClient)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		realms := api.Group("/realms").Name("Realms API")
 | 
							realms := api.Group("/realms").Name("Realms API")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
@@ -85,5 +91,9 @@ func MapAPIs(app *fiber.App) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			return c.Next()
 | 
								return c.Next()
 | 
				
			||||||
		}).Get("/ws", websocket.New(listenWebsocket))
 | 
							}).Get("/ws", websocket.New(listenWebsocket))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							api.All("/*", func(c *fiber.Ctx) error {
 | 
				
			||||||
 | 
								return fiber.ErrNotFound
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										140
									
								
								pkg/internal/server/ui/oauth.go → pkg/internal/server/api/oauth_api.go
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										140
									
								
								pkg/internal/server/ui/oauth.go → pkg/internal/server/api/oauth_api.go
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,111 +1,76 @@
 | 
				
			|||||||
package ui
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
	"github.com/nicksnyder/go-i18n/v2/i18n"
 | 
					 | 
				
			||||||
	"github.com/samber/lo"
 | 
					 | 
				
			||||||
	"github.com/sujit-baniya/flash"
 | 
					 | 
				
			||||||
	"html/template"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
 | 
				
			||||||
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
				
			||||||
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
				
			||||||
 | 
						"github.com/gofiber/fiber/v2"
 | 
				
			||||||
 | 
						"github.com/samber/lo"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func authorizePage(c *fiber.Ctx) error {
 | 
					func tryAuthorizeThirdClient(c *fiber.Ctx) error {
 | 
				
			||||||
	localizer := c.Locals("localizer").(*i18n.Localizer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
					 | 
				
			||||||
		return DoAuthRedirect(c)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user := c.Locals("user").(models.Account)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	id := c.Query("client_id")
 | 
						id := c.Query("client_id")
 | 
				
			||||||
	redirect := c.Query("redirect_uri")
 | 
						redirect := c.Query("redirect_uri")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var message string
 | 
					 | 
				
			||||||
	if len(id) <= 0 || len(redirect) <= 0 {
 | 
						if len(id) <= 0 || len(redirect) <= 0 {
 | 
				
			||||||
		message = "invalid request, missing query parameters"
 | 
							return fiber.NewError(fiber.StatusBadRequest, "invalid request, missing query parameters")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var client models.ThirdClient
 | 
						var client models.ThirdClient
 | 
				
			||||||
	if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
						if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
				
			||||||
		message = fmt.Sprintf("unable to find client: %v", err)
 | 
							return fiber.NewError(fiber.StatusNotFound, err.Error())
 | 
				
			||||||
	} else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) {
 | 
						} else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) {
 | 
				
			||||||
		message = "invalid callback url"
 | 
							return fiber.NewError(fiber.StatusBadRequest, "invalid callback url")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := exts.EnsureAuthenticated(c); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := c.Locals("user").(models.Account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var ticket models.AuthTicket
 | 
						var ticket models.AuthTicket
 | 
				
			||||||
	if err := database.C.Where(&models.AuthTicket{
 | 
						if err := database.C.Where(&models.AuthTicket{
 | 
				
			||||||
		AccountID: user.ID,
 | 
							AccountID: user.ID,
 | 
				
			||||||
		ClientID:  &client.ID,
 | 
							ClientID:  &client.ID,
 | 
				
			||||||
	}).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil {
 | 
						}).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil {
 | 
				
			||||||
		if !(ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix()) {
 | 
							if ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix() {
 | 
				
			||||||
			ticket, err = services.RegenSession(ticket)
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
			if c.Query("response_type") == "code" {
 | 
					 | 
				
			||||||
				return c.Redirect(fmt.Sprintf(
 | 
					 | 
				
			||||||
					"%s?code=%s&state=%s",
 | 
					 | 
				
			||||||
					redirect,
 | 
					 | 
				
			||||||
					*ticket.GrantToken,
 | 
					 | 
				
			||||||
					c.Query("state"),
 | 
					 | 
				
			||||||
				))
 | 
					 | 
				
			||||||
			} else if c.Query("response_type") == "token" {
 | 
					 | 
				
			||||||
				if access, refresh, err := services.GetToken(ticket); err == nil {
 | 
					 | 
				
			||||||
					return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s",
 | 
					 | 
				
			||||||
						redirect,
 | 
					 | 
				
			||||||
						access,
 | 
					 | 
				
			||||||
						refresh, c.Query("state"),
 | 
					 | 
				
			||||||
					))
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	decline, _ := localizer.LocalizeMessage(&i18n.Message{ID: "decline"})
 | 
					 | 
				
			||||||
	approve, _ := localizer.LocalizeMessage(&i18n.Message{ID: "approve"})
 | 
					 | 
				
			||||||
	title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeTitle"})
 | 
					 | 
				
			||||||
	caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeCaption"})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	qs := "/authorize?" + string(c.Request().URI().QueryString())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/authorize", fiber.Map{
 | 
					 | 
				
			||||||
		"info":       lo.Ternary[any](len(message) > 0, message, flash.Get(c)["message"]),
 | 
					 | 
				
			||||||
				"client": client,
 | 
									"client": client,
 | 
				
			||||||
		"scopes":     strings.Split(c.Query("scope"), " "),
 | 
									"ticket": nil,
 | 
				
			||||||
		"action_url": template.URL(qs),
 | 
								})
 | 
				
			||||||
		"i18n": fiber.Map{
 | 
							} else {
 | 
				
			||||||
			"approve": approve,
 | 
								ticket, err = services.RegenSession(ticket)
 | 
				
			||||||
			"decline": decline,
 | 
							}
 | 
				
			||||||
			"title":   title,
 | 
					
 | 
				
			||||||
			"caption": caption,
 | 
							return c.JSON(fiber.Map{
 | 
				
			||||||
		},
 | 
								"client": client,
 | 
				
			||||||
	}, "views/layouts/auth")
 | 
								"ticket": ticket,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return c.JSON(fiber.Map{
 | 
				
			||||||
 | 
							"client": client,
 | 
				
			||||||
 | 
							"ticket": nil,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func authorizeAction(c *fiber.Ctx) error {
 | 
					func authorizeThirdClient(c *fiber.Ctx) error {
 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user := c.Locals("user").(models.Account)
 | 
					 | 
				
			||||||
	id := c.Query("client_id")
 | 
						id := c.Query("client_id")
 | 
				
			||||||
	response := c.Query("response_type")
 | 
						response := c.Query("response_type")
 | 
				
			||||||
	redirect := c.Query("redirect_uri")
 | 
						redirect := c.Query("redirect_uri")
 | 
				
			||||||
	scope := c.Query("scope")
 | 
						scope := c.Query("scope")
 | 
				
			||||||
 | 
						if len(scope) <= 0 {
 | 
				
			||||||
 | 
							return fiber.NewError(fiber.StatusBadRequest, "invalid request params")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
						if err := exts.EnsureAuthenticated(c); err != nil {
 | 
				
			||||||
		return DoAuthRedirect(c)
 | 
							return err
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	redirectBackUri := "/authorize?" + string(c.Request().URI().QueryString())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(scope) <= 0 {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "invalid request parameters",
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						user := c.Locals("user").(models.Account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var client models.ThirdClient
 | 
						var client models.ThirdClient
 | 
				
			||||||
	if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
						if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
 | 
				
			||||||
@@ -128,12 +93,10 @@ func authorizeAction(c *fiber.Ctx) error {
 | 
				
			|||||||
			return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
								return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
								services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
				
			||||||
			return c.Redirect(fmt.Sprintf(
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
				"%s?code=%s&state=%s",
 | 
									"ticket":       ticket,
 | 
				
			||||||
				redirect,
 | 
									"redirect_uri": redirect,
 | 
				
			||||||
				*ticket.GrantToken,
 | 
								})
 | 
				
			||||||
				c.Query("state"),
 | 
					 | 
				
			||||||
			))
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	case "token":
 | 
						case "token":
 | 
				
			||||||
		// OAuth Implicit Mode
 | 
							// OAuth Implicit Mode
 | 
				
			||||||
@@ -152,15 +115,14 @@ func authorizeAction(c *fiber.Ctx) error {
 | 
				
			|||||||
			return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
								return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
								services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
				
			||||||
			return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s",
 | 
								return c.JSON(fiber.Map{
 | 
				
			||||||
				redirect,
 | 
									"access_token":  access,
 | 
				
			||||||
				access,
 | 
									"refresh_token": refresh,
 | 
				
			||||||
				refresh, c.Query("state"),
 | 
									"redirect_uri":  redirect,
 | 
				
			||||||
			))
 | 
									"ticket":        ticket,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
							return fiber.NewError(fiber.StatusBadRequest, "unsupported response type")
 | 
				
			||||||
			"message": "unsupported response type",
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,76 +0,0 @@
 | 
				
			|||||||
package api
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func getPersonalPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	alias := c.Params("alias")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var account models.Account
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(&models.Account{Name: alias}).
 | 
					 | 
				
			||||||
		First(&account).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var page models.AccountPage
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(&models.AccountPage{AccountID: account.ID}).
 | 
					 | 
				
			||||||
		First(&page).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.JSON(page)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func getOwnPersonalPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user := c.Locals("user").(models.Account)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var page models.AccountPage
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(&models.AccountPage{AccountID: user.ID}).
 | 
					 | 
				
			||||||
		FirstOrCreate(&page, &models.AccountPage{AccountID: user.ID}).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusBadRequest, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.JSON(page)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func editPersonalPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user := c.Locals("user").(models.Account)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var data struct {
 | 
					 | 
				
			||||||
		Content string                    `json:"content"`
 | 
					 | 
				
			||||||
		Links   []models.AccountPageLinks `json:"links"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := exts.BindAndValidate(c, &data); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var page models.AccountPage
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(&models.AccountPage{AccountID: user.ID}).
 | 
					 | 
				
			||||||
		FirstOrInit(&page).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	page.Content = data.Content
 | 
					 | 
				
			||||||
	page.Links = data.Links
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := database.C.Save(&page).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.SendStatus(fiber.StatusOK)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,32 +1,31 @@
 | 
				
			|||||||
package server
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/server/admin"
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/api"
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/server/api"
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
				
			||||||
 | 
						"github.com/gofiber/fiber/v2/middleware/filesystem"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/i18n"
 | 
						"git.solsynth.dev/hydrogen/passport/pkg/internal/i18n"
 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/admin"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/ui"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
						"github.com/gofiber/fiber/v2"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2/middleware/cors"
 | 
						"github.com/gofiber/fiber/v2/middleware/cors"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2/middleware/favicon"
 | 
						"github.com/gofiber/fiber/v2/middleware/favicon"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2/middleware/idempotency"
 | 
						"github.com/gofiber/fiber/v2/middleware/idempotency"
 | 
				
			||||||
	"github.com/gofiber/fiber/v2/middleware/logger"
 | 
						"github.com/gofiber/fiber/v2/middleware/logger"
 | 
				
			||||||
	"github.com/gofiber/template/html/v2"
 | 
					 | 
				
			||||||
	jsoniter "github.com/json-iterator/go"
 | 
						jsoniter "github.com/json-iterator/go"
 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
	"github.com/spf13/viper"
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var A *fiber.App
 | 
					type HTTPApp struct {
 | 
				
			||||||
 | 
						app *fiber.App
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewServer() {
 | 
					func NewServer() *HTTPApp {
 | 
				
			||||||
	templates := html.NewFileSystem(http.FS(pkg.FS), ".gohtml")
 | 
						app := fiber.New(fiber.Config{
 | 
				
			||||||
 | 
					 | 
				
			||||||
	A = fiber.New(fiber.Config{
 | 
					 | 
				
			||||||
		DisableStartupMessage: true,
 | 
							DisableStartupMessage: true,
 | 
				
			||||||
		EnableIPValidation:    true,
 | 
							EnableIPValidation:    true,
 | 
				
			||||||
		ServerHeader:          "Hydrogen.Passport",
 | 
							ServerHeader:          "Hydrogen.Passport",
 | 
				
			||||||
@@ -35,12 +34,10 @@ func NewServer() {
 | 
				
			|||||||
		JSONEncoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
 | 
							JSONEncoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
 | 
				
			||||||
		JSONDecoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
 | 
							JSONDecoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
 | 
				
			||||||
		EnablePrintRoutes:     viper.GetBool("debug.print_routes"),
 | 
							EnablePrintRoutes:     viper.GetBool("debug.print_routes"),
 | 
				
			||||||
		Views:                 templates,
 | 
					 | 
				
			||||||
		ViewsLayout:           "views/index",
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	A.Use(idempotency.New())
 | 
						app.Use(idempotency.New())
 | 
				
			||||||
	A.Use(cors.New(cors.Config{
 | 
						app.Use(cors.New(cors.Config{
 | 
				
			||||||
		AllowCredentials: true,
 | 
							AllowCredentials: true,
 | 
				
			||||||
		AllowMethods: strings.Join([]string{
 | 
							AllowMethods: strings.Join([]string{
 | 
				
			||||||
			fiber.MethodGet,
 | 
								fiber.MethodGet,
 | 
				
			||||||
@@ -56,27 +53,34 @@ func NewServer() {
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
	}))
 | 
						}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	A.Use(logger.New(logger.Config{
 | 
						app.Use(logger.New(logger.Config{
 | 
				
			||||||
		Format: "${status} | ${latency} | ${method} ${path}\n",
 | 
							Format: "${status} | ${latency} | ${method} ${path}\n",
 | 
				
			||||||
		Output: log.Logger,
 | 
							Output: log.Logger,
 | 
				
			||||||
	}))
 | 
						}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	A.Use(exts.AuthMiddleware)
 | 
						app.Use(exts.AuthMiddleware)
 | 
				
			||||||
	A.Use(i18n.I18nMiddleware)
 | 
						app.Use(i18n.I18nMiddleware)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	A.Use(favicon.New(favicon.Config{
 | 
						api.MapAPIs(app)
 | 
				
			||||||
		FileSystem: http.FS(pkg.FS),
 | 
						admin.MapAdminEndpoints(app)
 | 
				
			||||||
		File:       "views/favicon.png",
 | 
					
 | 
				
			||||||
 | 
						app.Use(filesystem.New(filesystem.Config{
 | 
				
			||||||
 | 
							Root:         http.Dir(viper.GetString("frontend_app")),
 | 
				
			||||||
 | 
							Index:        "index.html",
 | 
				
			||||||
 | 
							NotFoundFile: "index.html",
 | 
				
			||||||
 | 
							MaxAge:       3600,
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.Use(favicon.New(favicon.Config{
 | 
				
			||||||
 | 
							File: filepath.Join(viper.GetString("frontend_app"), "favicon.png"),
 | 
				
			||||||
		URL:  "/favicon.png",
 | 
							URL:  "/favicon.png",
 | 
				
			||||||
	}))
 | 
						}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	api.MapAPIs(A)
 | 
						return &HTTPApp{app}
 | 
				
			||||||
	admin.MapAdminEndpoints(A)
 | 
					 | 
				
			||||||
	ui.MapUserInterface(A)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Listen() {
 | 
					func (v *HTTPApp) Listen() {
 | 
				
			||||||
	if err := A.Listen(viper.GetString("bind")); err != nil {
 | 
						if err := v.app.Listen(viper.GetString("bind")); err != nil {
 | 
				
			||||||
		log.Fatal().Err(err).Msg("An error occurred when starting server...")
 | 
							log.Fatal().Err(err).Msg("An error occurred when starting server...")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,55 +0,0 @@
 | 
				
			|||||||
package ui
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
					 | 
				
			||||||
	"html/template"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
	"github.com/gomarkdown/markdown"
 | 
					 | 
				
			||||||
	"github.com/gomarkdown/markdown/html"
 | 
					 | 
				
			||||||
	"github.com/gomarkdown/markdown/parser"
 | 
					 | 
				
			||||||
	"github.com/sujit-baniya/flash"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func selfUserinfoPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	if err := exts.EnsureAuthenticated(c); err != nil {
 | 
					 | 
				
			||||||
		return DoAuthRedirect(c)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user := c.Locals("user").(models.Account)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var data models.Account
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
 | 
					 | 
				
			||||||
		Preload("Profile").
 | 
					 | 
				
			||||||
		Preload("PersonalPage").
 | 
					 | 
				
			||||||
		Preload("Contacts").
 | 
					 | 
				
			||||||
		First(&data).Error; err != nil {
 | 
					 | 
				
			||||||
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	birthday := "Unknown"
 | 
					 | 
				
			||||||
	if data.Profile.Birthday != nil {
 | 
					 | 
				
			||||||
		birthday = data.Profile.Birthday.Format(time.RFC822)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	doc := parser.
 | 
					 | 
				
			||||||
		NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock).
 | 
					 | 
				
			||||||
		Parse([]byte(data.PersonalPage.Content))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/users/me", fiber.Map{
 | 
					 | 
				
			||||||
		"info":          flash.Get(c)["message"],
 | 
					 | 
				
			||||||
		"uid":           fmt.Sprintf("%08d", data.ID),
 | 
					 | 
				
			||||||
		"joined_at":     data.CreatedAt.Format(time.RFC822),
 | 
					 | 
				
			||||||
		"birthday_at":   birthday,
 | 
					 | 
				
			||||||
		"personal_page": template.HTML(markdown.Render(doc, renderer)),
 | 
					 | 
				
			||||||
		"userinfo":      data,
 | 
					 | 
				
			||||||
		"avatar":        data.GetAvatar(),
 | 
					 | 
				
			||||||
		"banner":        data.GetBanner(),
 | 
					 | 
				
			||||||
	}, "views/layouts/user-center")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,34 +0,0 @@
 | 
				
			|||||||
package ui
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func DoAuthRedirect(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	uri := c.Request().URI().FullURI()
 | 
					 | 
				
			||||||
	return c.Redirect(fmt.Sprintf("/sign-in?redirect_uri=%s", string(uri)))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func MapUserInterface(app *fiber.App) {
 | 
					 | 
				
			||||||
	pages := app.Group("/").Name("Pages")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pages.Get("/", func(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
		return c.Redirect("/users/me")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pages.Get("/sign-up", signupPage)
 | 
					 | 
				
			||||||
	pages.Get("/sign-in", signinPage)
 | 
					 | 
				
			||||||
	pages.Get("/mfa", mfaRequestPage)
 | 
					 | 
				
			||||||
	pages.Get("/mfa/apply", mfaApplyPage)
 | 
					 | 
				
			||||||
	pages.Get("/authorize", authorizePage)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pages.Post("/sign-up", signupAction)
 | 
					 | 
				
			||||||
	pages.Post("/sign-in", signinAction)
 | 
					 | 
				
			||||||
	pages.Post("/mfa", mfaRequestAction)
 | 
					 | 
				
			||||||
	pages.Post("/mfa/apply", mfaApplyAction)
 | 
					 | 
				
			||||||
	pages.Post("/authorize", authorizeAction)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pages.Get("/users/me", selfUserinfoPage)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,194 +0,0 @@
 | 
				
			|||||||
package ui
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
	"github.com/nicksnyder/go-i18n/v2/i18n"
 | 
					 | 
				
			||||||
	"github.com/samber/lo"
 | 
					 | 
				
			||||||
	"github.com/sujit-baniya/flash"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func mfaRequestPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	ticketId := c.QueryInt("ticket", 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err := services.GetTicket(uint(ticketId))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "you must provide ticket id to perform multi-factor authenticate",
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	user, err := services.GetAccount(ticket.AccountID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "ticket related user just weirdly disappear",
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	factors, err := services.ListUserFactor(user.ID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable to get your factors: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	factors = lo.Filter(factors, func(item models.AuthFactor, index int) bool {
 | 
					 | 
				
			||||||
		return item.Type != models.PasswordAuthFactor
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	localizer := c.Locals("localizer").(*i18n.Localizer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
 | 
					 | 
				
			||||||
	title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"})
 | 
					 | 
				
			||||||
	caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/mfa", fiber.Map{
 | 
					 | 
				
			||||||
		"info":         flash.Get(c)["message"],
 | 
					 | 
				
			||||||
		"redirect_uri": flash.Get(c)["redirect_uri"],
 | 
					 | 
				
			||||||
		"ticket_id":    ticket.ID,
 | 
					 | 
				
			||||||
		"factors": lo.Map(factors, func(item models.AuthFactor, index int) fiber.Map {
 | 
					 | 
				
			||||||
			return fiber.Map{
 | 
					 | 
				
			||||||
				"name": services.GetFactorName(item.Type, localizer),
 | 
					 | 
				
			||||||
				"id":   item.ID,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}),
 | 
					 | 
				
			||||||
		"i18n": fiber.Map{
 | 
					 | 
				
			||||||
			"next":    next,
 | 
					 | 
				
			||||||
			"title":   title,
 | 
					 | 
				
			||||||
			"caption": caption,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}, "views/layouts/auth")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func mfaRequestAction(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	var data struct {
 | 
					 | 
				
			||||||
		TicketID uint `form:"ticket_id" validate:"required"`
 | 
					 | 
				
			||||||
		FactorID uint `form:"factor_id" validate:"required"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	redirectBackUri := "/sign-in"
 | 
					 | 
				
			||||||
	err := exts.BindAndValidate(c, &data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if data.TicketID > 0 {
 | 
					 | 
				
			||||||
		redirectBackUri = fmt.Sprintf("/mfa?ticket=%d", data.TicketID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": err.Error(),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	factor, err := services.GetFactor(data.FactorID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("factor was not found: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	_, err = services.GetFactorCode(factor)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable to get factor code: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return flash.WithData(c, fiber.Map{
 | 
					 | 
				
			||||||
		"redirect_uri": exts.GetRedirectUri(c),
 | 
					 | 
				
			||||||
	}).Redirect(fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, factor.ID))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func mfaApplyPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	ticketId := c.QueryInt("ticket", 0)
 | 
					 | 
				
			||||||
	factorId := c.QueryInt("factor", 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err := services.GetTicket(uint(ticketId))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable to find your ticket: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	factor, err := services.GetFactor(uint(factorId))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable to find your factors: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	localizer := c.Locals("localizer").(*i18n.Localizer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
 | 
					 | 
				
			||||||
	password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
 | 
					 | 
				
			||||||
	title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"})
 | 
					 | 
				
			||||||
	caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/mfa-apply", fiber.Map{
 | 
					 | 
				
			||||||
		"info":      flash.Get(c)["message"],
 | 
					 | 
				
			||||||
		"label":     services.GetFactorName(factor.Type, localizer),
 | 
					 | 
				
			||||||
		"ticket_id": ticket.ID,
 | 
					 | 
				
			||||||
		"factor_id": factor.ID,
 | 
					 | 
				
			||||||
		"i18n": fiber.Map{
 | 
					 | 
				
			||||||
			"next":     next,
 | 
					 | 
				
			||||||
			"password": password,
 | 
					 | 
				
			||||||
			"title":    title,
 | 
					 | 
				
			||||||
			"caption":  caption,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}, "views/layouts/auth")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func mfaApplyAction(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	var data struct {
 | 
					 | 
				
			||||||
		TicketID uint   `form:"ticket_id" validate:"required"`
 | 
					 | 
				
			||||||
		FactorID uint   `form:"factor_id" validate:"required"`
 | 
					 | 
				
			||||||
		Code     string `form:"code" validate:"required"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	redirectBackUri := "/sign-in"
 | 
					 | 
				
			||||||
	err := exts.BindAndValidate(c, &data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if data.TicketID > 0 {
 | 
					 | 
				
			||||||
		redirectBackUri = fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, data.FactorID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": err.Error(),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err := services.GetTicket(data.TicketID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable to find your ticket: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	factor, err := services.GetFactor(data.FactorID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("factor was not found: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("invalid multi-factor authenticate code: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect(redirectBackUri)
 | 
					 | 
				
			||||||
	} else if ticket.IsAvailable() != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "ticket weirdly still unavailable after multi-factor authenticate",
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	access, refresh, err := services.ExchangeToken(*ticket.GrantToken)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("failed to exchange token: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		exts.SetAuthCookies(c, access, refresh)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/users/me")))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,93 +0,0 @@
 | 
				
			|||||||
package ui
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
	"github.com/nicksnyder/go-i18n/v2/i18n"
 | 
					 | 
				
			||||||
	"github.com/samber/lo"
 | 
					 | 
				
			||||||
	"github.com/sujit-baniya/flash"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func signinPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	localizer := c.Locals("localizer").(*i18n.Localizer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
 | 
					 | 
				
			||||||
	username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"})
 | 
					 | 
				
			||||||
	password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
 | 
					 | 
				
			||||||
	signup, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
 | 
					 | 
				
			||||||
	title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
 | 
					 | 
				
			||||||
	caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"})
 | 
					 | 
				
			||||||
	requiredNotify, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinRequired"})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var info any
 | 
					 | 
				
			||||||
	if flash.Get(c)["message"] != nil {
 | 
					 | 
				
			||||||
		info = flash.Get(c)["message"]
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		info = requiredNotify
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/signin", fiber.Map{
 | 
					 | 
				
			||||||
		"info": info,
 | 
					 | 
				
			||||||
		"i18n": fiber.Map{
 | 
					 | 
				
			||||||
			"next":     next,
 | 
					 | 
				
			||||||
			"username": username,
 | 
					 | 
				
			||||||
			"password": password,
 | 
					 | 
				
			||||||
			"signup":   signup,
 | 
					 | 
				
			||||||
			"title":    title,
 | 
					 | 
				
			||||||
			"caption":  caption,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}, "views/layouts/auth")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func signinAction(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	var data struct {
 | 
					 | 
				
			||||||
		Username string `form:"username" validate:"required"`
 | 
					 | 
				
			||||||
		Password string `form:"password" validate:"required"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := exts.BindAndValidate(c, &data); err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": err.Error(),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user, err := services.LookupAccount(data.Username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("account was not found: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("unable setup ticket: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("invalid password: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if ticket.IsAvailable() != nil {
 | 
					 | 
				
			||||||
		return flash.WithData(c, fiber.Map{
 | 
					 | 
				
			||||||
			"redirect_uri": exts.GetRedirectUri(c),
 | 
					 | 
				
			||||||
		}).Redirect(fmt.Sprintf("/mfa?ticket=%d", ticket.ID))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	access, refresh, err := services.ExchangeToken(*ticket.GrantToken)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": fmt.Sprintf("failed to exchange token: %v", err.Error()),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-in")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		exts.SetAuthCookies(c, access, refresh)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/users/me")))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,87 +0,0 @@
 | 
				
			|||||||
package ui
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
 | 
					 | 
				
			||||||
	"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
 | 
					 | 
				
			||||||
	"github.com/gofiber/fiber/v2"
 | 
					 | 
				
			||||||
	"github.com/nicksnyder/go-i18n/v2/i18n"
 | 
					 | 
				
			||||||
	"github.com/samber/lo"
 | 
					 | 
				
			||||||
	"github.com/spf13/viper"
 | 
					 | 
				
			||||||
	"github.com/sujit-baniya/flash"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func signupPage(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	localizer := c.Locals("localizer").(*i18n.Localizer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
 | 
					 | 
				
			||||||
	email, _ := localizer.LocalizeMessage(&i18n.Message{ID: "email"})
 | 
					 | 
				
			||||||
	nickname, _ := localizer.LocalizeMessage(&i18n.Message{ID: "nickname"})
 | 
					 | 
				
			||||||
	username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"})
 | 
					 | 
				
			||||||
	password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
 | 
					 | 
				
			||||||
	magicToken, _ := localizer.LocalizeMessage(&i18n.Message{ID: "magicToken"})
 | 
					 | 
				
			||||||
	signin, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
 | 
					 | 
				
			||||||
	title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
 | 
					 | 
				
			||||||
	caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupCaption"})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.Render("views/signup", fiber.Map{
 | 
					 | 
				
			||||||
		"info":            flash.Get(c)["message"],
 | 
					 | 
				
			||||||
		"use_magic_token": viper.GetBool("use_registration_magic_token"),
 | 
					 | 
				
			||||||
		"i18n": fiber.Map{
 | 
					 | 
				
			||||||
			"next":        next,
 | 
					 | 
				
			||||||
			"email":       email,
 | 
					 | 
				
			||||||
			"username":    username,
 | 
					 | 
				
			||||||
			"nickname":    nickname,
 | 
					 | 
				
			||||||
			"password":    password,
 | 
					 | 
				
			||||||
			"magic_token": magicToken,
 | 
					 | 
				
			||||||
			"signin":      signin,
 | 
					 | 
				
			||||||
			"title":       title,
 | 
					 | 
				
			||||||
			"caption":     caption,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}, "views/layouts/auth")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func signupAction(c *fiber.Ctx) error {
 | 
					 | 
				
			||||||
	var data struct {
 | 
					 | 
				
			||||||
		Name       string `form:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
 | 
					 | 
				
			||||||
		Nick       string `form:"nick" validate:"required,min=4,max=24"`
 | 
					 | 
				
			||||||
		Email      string `form:"email" validate:"required,email"`
 | 
					 | 
				
			||||||
		Password   string `form:"password" validate:"required,min=4,max=32"`
 | 
					 | 
				
			||||||
		MagicToken string `form:"magic_token"`
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := exts.BindAndValidate(c, &data); err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": err.Error(),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-up")
 | 
					 | 
				
			||||||
	} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "magic token was required",
 | 
					 | 
				
			||||||
		}).Redirect("/sign-up")
 | 
					 | 
				
			||||||
	} else if viper.GetBool("use_registration_magic_token") {
 | 
					 | 
				
			||||||
		if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil {
 | 
					 | 
				
			||||||
			return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
				"message": fmt.Sprintf("magic token was invalid: %v", err.Error()),
 | 
					 | 
				
			||||||
			}).Redirect("/sign-up")
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			database.C.Delete(&tk)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if _, err := services.CreateAccount(
 | 
					 | 
				
			||||||
		data.Name,
 | 
					 | 
				
			||||||
		data.Nick,
 | 
					 | 
				
			||||||
		data.Email,
 | 
					 | 
				
			||||||
		data.Password,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": err.Error(),
 | 
					 | 
				
			||||||
		}).Redirect("/sign-up")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return flash.WithInfo(c, fiber.Map{
 | 
					 | 
				
			||||||
			"message": "account has been created. now you can sign in!",
 | 
					 | 
				
			||||||
		}).Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/sign-in")))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -2,6 +2,7 @@ package services
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
	"github.com/spf13/viper"
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
	"gorm.io/datatypes"
 | 
						"gorm.io/datatypes"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -93,7 +94,7 @@ func ConfirmAccount(code string) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var user models.Account
 | 
						var user models.Account
 | 
				
			||||||
	if err := database.C.Where(&models.Account{
 | 
						if err := database.C.Where(&models.Account{
 | 
				
			||||||
		BaseModel: models.BaseModel{ID: *token.AssignTo},
 | 
							BaseModel: models.BaseModel{ID: *token.AccountID},
 | 
				
			||||||
	}).First(&user).Error; err != nil {
 | 
						}).First(&user).Error; err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -121,3 +122,49 @@ func ConfirmAccount(code string) error {
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DeleteAccount(id uint) error {
 | 
				
			||||||
 | 
						tx := database.C.Begin()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, model := range []any{
 | 
				
			||||||
 | 
							&models.Badge{},
 | 
				
			||||||
 | 
							&models.RealmMember{},
 | 
				
			||||||
 | 
							&models.AccountContact{},
 | 
				
			||||||
 | 
							&models.AuthFactor{},
 | 
				
			||||||
 | 
							&models.AuthTicket{},
 | 
				
			||||||
 | 
							&models.MagicToken{},
 | 
				
			||||||
 | 
							&models.ThirdClient{},
 | 
				
			||||||
 | 
							&models.Notification{},
 | 
				
			||||||
 | 
							&models.NotificationSubscriber{},
 | 
				
			||||||
 | 
							&models.AccountFriendship{},
 | 
				
			||||||
 | 
						} {
 | 
				
			||||||
 | 
							if err := tx.Delete(model, "account_id = ?", id).Error; err != nil {
 | 
				
			||||||
 | 
								tx.Rollback()
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := tx.Delete(&models.Account{}, "id = ?", id).Error; err != nil {
 | 
				
			||||||
 | 
							tx.Rollback()
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tx.Commit().Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func RecycleUnConfirmAccount() {
 | 
				
			||||||
 | 
						var hitList []models.Account
 | 
				
			||||||
 | 
						if err := database.C.Where("confirmed_at IS NULL").Find(&hitList).Error; err != nil {
 | 
				
			||||||
 | 
							log.Error().Err(err).Msg("An error occurred while recycling accounts...")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(hitList) > 0 {
 | 
				
			||||||
 | 
							log.Info().Int("count", len(hitList)).Msg("Going to recycle those un-confirmed accounts...")
 | 
				
			||||||
 | 
							for _, entry := range hitList {
 | 
				
			||||||
 | 
								if err := DeleteAccount(entry.ID); err != nil {
 | 
				
			||||||
 | 
									log.Error().Err(err).Msg("An error occurred while recycling accounts...")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,22 +12,13 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func DetectRisk(user models.Account, ip, ua string) bool {
 | 
					func DetectRisk(user models.Account, ip, ua string) bool {
 | 
				
			||||||
	var availableFactor int64
 | 
						var clue int64
 | 
				
			||||||
	if err := database.C.
 | 
					 | 
				
			||||||
		Where(models.AuthFactor{AccountID: user.ID}).
 | 
					 | 
				
			||||||
		Where("type != ?", models.PasswordAuthFactor).
 | 
					 | 
				
			||||||
		Model(models.AuthFactor{}).
 | 
					 | 
				
			||||||
		Where(&availableFactor); err != nil || availableFactor <= 0 {
 | 
					 | 
				
			||||||
		return false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var secureFactor int64
 | 
					 | 
				
			||||||
	if err := database.C.
 | 
						if err := database.C.
 | 
				
			||||||
		Where(models.AuthTicket{AccountID: user.ID, IpAddress: ip}).
 | 
							Where(models.AuthTicket{AccountID: user.ID, IpAddress: ip}).
 | 
				
			||||||
		Where("available_at IS NOT NULL").
 | 
							Where("available_at IS NOT NULL").
 | 
				
			||||||
		Model(models.AuthTicket{}).
 | 
							Model(models.AuthTicket{}).
 | 
				
			||||||
		Count(&secureFactor).Error; err == nil {
 | 
							Count(&clue).Error; err == nil {
 | 
				
			||||||
		if secureFactor >= 1 {
 | 
							if clue >= 1 {
 | 
				
			||||||
			return false
 | 
								return false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -81,7 +72,7 @@ func NewOauthTicket(
 | 
				
			|||||||
		AccessToken:  lo.ToPtr(uuid.NewString()),
 | 
							AccessToken:  lo.ToPtr(uuid.NewString()),
 | 
				
			||||||
		RefreshToken: lo.ToPtr(uuid.NewString()),
 | 
							RefreshToken: lo.ToPtr(uuid.NewString()),
 | 
				
			||||||
		AvailableAt:  lo.ToPtr(time.Now()),
 | 
							AvailableAt:  lo.ToPtr(time.Now()),
 | 
				
			||||||
		ExpiredAt:    lo.ToPtr(time.Now()),
 | 
							ExpiredAt:    lo.ToPtr(time.Now().Add(7 * 24 * time.Hour)),
 | 
				
			||||||
		ClientID:     &client.ID,
 | 
							ClientID:     &client.ID,
 | 
				
			||||||
		AccountID:    user.ID,
 | 
							AccountID:    user.ID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expired
 | 
				
			|||||||
	token := models.MagicToken{
 | 
						token := models.MagicToken{
 | 
				
			||||||
		Code:      strings.Replace(uuid.NewString(), "-", "", -1),
 | 
							Code:      strings.Replace(uuid.NewString(), "-", "", -1),
 | 
				
			||||||
		Type:      mode,
 | 
							Type:      mode,
 | 
				
			||||||
		AssignTo:  &uid,
 | 
							AccountID: &uid,
 | 
				
			||||||
		ExpiredAt: expiredAt,
 | 
							ExpiredAt: expiredAt,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,13 +59,13 @@ func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expired
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NotifyMagicToken(token models.MagicToken) error {
 | 
					func NotifyMagicToken(token models.MagicToken) error {
 | 
				
			||||||
	if token.AssignTo == nil {
 | 
						if token.AccountID == nil {
 | 
				
			||||||
		return fmt.Errorf("could notify a non-assign magic token")
 | 
							return fmt.Errorf("could notify a non-assign magic token")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var user models.Account
 | 
						var user models.Account
 | 
				
			||||||
	if err := database.C.Where(&models.Account{
 | 
						if err := database.C.Where(&models.Account{
 | 
				
			||||||
		BaseModel: models.BaseModel{ID: *token.AssignTo},
 | 
							BaseModel: models.BaseModel{ID: *token.AccountID},
 | 
				
			||||||
	}).Preload("Contacts").First(&user).Error; err != nil {
 | 
						}).Preload("Contacts").First(&user).Error; err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,56 +0,0 @@
 | 
				
			|||||||
<div class="left-part">
 | 
					 | 
				
			||||||
  <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h1 class="title">{{.i18n.title}} {{.client.Name}}</h1>
 | 
					 | 
				
			||||||
  <p class="caption">{{.i18n.caption}}</p>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <div class="responsive-title-gap "></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <form class="action-form" action="{{.action_url}}" method="POST">
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
      <div class="section-title">Description</div>
 | 
					 | 
				
			||||||
      <div class="section-body">{{.client.Description}}</div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
      <div class="section-title">Requested scopes</div>
 | 
					 | 
				
			||||||
      <ul class="section-scope list-group">
 | 
					 | 
				
			||||||
        {{range $_, $element := .scopes}}
 | 
					 | 
				
			||||||
        <li class="monospace list-group-item">
 | 
					 | 
				
			||||||
          {{$element}}
 | 
					 | 
				
			||||||
        </li>
 | 
					 | 
				
			||||||
        {{end}}
 | 
					 | 
				
			||||||
      </ul>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="action-form-buttons">
 | 
					 | 
				
			||||||
      <button class="btn btn-secondary" type="button" id="decline-button">{{.i18n.decline}}</button>
 | 
					 | 
				
			||||||
      <button class="btn btn-primary" type="submit">{{.i18n.approve}}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  .section-title {
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .section-scope {
 | 
					 | 
				
			||||||
    margin-top: 4px;
 | 
					 | 
				
			||||||
    margin-left: -8px;
 | 
					 | 
				
			||||||
    margin-right: -8px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .monospace {
 | 
					 | 
				
			||||||
    font-family: "Roboto Mono", monospace;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
  $("#decline-button").on("click", () => {
 | 
					 | 
				
			||||||
    history.back()
 | 
					 | 
				
			||||||
    window.close()
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 74 KiB  | 
@@ -1,10 +0,0 @@
 | 
				
			|||||||
<!doctype html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{template "views/partials/header"}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
{{embed}}
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@@ -1,115 +0,0 @@
 | 
				
			|||||||
<!doctype html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{template "views/partials/header"}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
  <div class="outer-container">
 | 
					 | 
				
			||||||
    <div class="inner-container">
 | 
					 | 
				
			||||||
      {{if ne .info nil}}
 | 
					 | 
				
			||||||
      <div class="alert alert-primary" role="alert">
 | 
					 | 
				
			||||||
        <svg class="bi me-2" role="img" aria-label="Info:">
 | 
					 | 
				
			||||||
          <use xlink:href="#info-fill" />
 | 
					 | 
				
			||||||
        </svg>
 | 
					 | 
				
			||||||
        <div class="content">{{.info}}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {{end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="card card-container">
 | 
					 | 
				
			||||||
        {{embed}}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  .outer-container {
 | 
					 | 
				
			||||||
    width: 100dvw;
 | 
					 | 
				
			||||||
    height: 100dvh;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .inner-container {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    min-width: 0;
 | 
					 | 
				
			||||||
    max-width: min(800px, 100dvw);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    margin: 1rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .card-container {
 | 
					 | 
				
			||||||
    transition: all .3s;
 | 
					 | 
				
			||||||
    height: auto;
 | 
					 | 
				
			||||||
    overflow: auto;
 | 
					 | 
				
			||||||
    display: grid;
 | 
					 | 
				
			||||||
    grid-template-columns: 1fr;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    padding: 48px;
 | 
					 | 
				
			||||||
    gap: 0 2rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .logo {
 | 
					 | 
				
			||||||
    margin-left: -8px;
 | 
					 | 
				
			||||||
    margin-bottom: -8px;
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .title {
 | 
					 | 
				
			||||||
    margin-block-start: 0.33em;
 | 
					 | 
				
			||||||
    margin-block-end: 0.33em;
 | 
					 | 
				
			||||||
    font-size: 2.5rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .caption {
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .action-form {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 0.8rem 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .action-form-buttons {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    gap: 4px;
 | 
					 | 
				
			||||||
    margin-top: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .action-form-buttons * {
 | 
					 | 
				
			||||||
    flex: 1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .block-field {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .responsive-hidden {
 | 
					 | 
				
			||||||
    display: unset;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .columns-two {
 | 
					 | 
				
			||||||
    display: grid;
 | 
					 | 
				
			||||||
    grid-template-columns: 1fr 1fr;
 | 
					 | 
				
			||||||
    gap: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (min-width: 768px) {
 | 
					 | 
				
			||||||
    .card-container {
 | 
					 | 
				
			||||||
      grid-template-columns: 1fr 1fr;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .responsive-title-gap {
 | 
					 | 
				
			||||||
      height: calc(56px + 0.44rem);
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@@ -1,128 +0,0 @@
 | 
				
			|||||||
<!doctype html>
 | 
					 | 
				
			||||||
<html lang="en">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{{template "views/partials/header"}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
  <div class="outer-container">
 | 
					 | 
				
			||||||
    <div class="inner-container">
 | 
					 | 
				
			||||||
      {{if ne .info nil}}
 | 
					 | 
				
			||||||
      <div class="alert alert-primary" role="alert">
 | 
					 | 
				
			||||||
        <svg class="bi me-2" role="img" aria-label="Info:">
 | 
					 | 
				
			||||||
          <use xlink:href="#info-fill" />
 | 
					 | 
				
			||||||
        </svg>
 | 
					 | 
				
			||||||
        <div class="content">{{.info}}</div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {{end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="card card-container">
 | 
					 | 
				
			||||||
        {{embed}}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  body,
 | 
					 | 
				
			||||||
  .outer-container {
 | 
					 | 
				
			||||||
    scrollbar-width: none;
 | 
					 | 
				
			||||||
    overflow-x: hidden;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .outer-container {
 | 
					 | 
				
			||||||
    width: 100dvw;
 | 
					 | 
				
			||||||
    min-height: 100dvh;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .outer-container::-webkit-scrollbar,
 | 
					 | 
				
			||||||
  body::-webkit-scrollbar {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
    width: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .inner-container {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
    min-width: 0;
 | 
					 | 
				
			||||||
    max-width: min(800px, 100dvw);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    margin: 1rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 1rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    padding-top: 1rem;
 | 
					 | 
				
			||||||
    padding-bottom: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .card-container {
 | 
					 | 
				
			||||||
    transition: all .3s;
 | 
					 | 
				
			||||||
    height: auto;
 | 
					 | 
				
			||||||
    overflow: auto;
 | 
					 | 
				
			||||||
    display: grid;
 | 
					 | 
				
			||||||
    grid-template-columns: 1fr;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    padding: 48px;
 | 
					 | 
				
			||||||
    gap: 0 2rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .logo {
 | 
					 | 
				
			||||||
    margin-left: -8px;
 | 
					 | 
				
			||||||
    margin-bottom: -8px;
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .title {
 | 
					 | 
				
			||||||
    margin-block-start: 0.33em;
 | 
					 | 
				
			||||||
    margin-block-end: 0.33em;
 | 
					 | 
				
			||||||
    font-size: 2.5rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .caption {
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .action-form {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 0.8rem 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .action-form-buttons {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: end;
 | 
					 | 
				
			||||||
    margin-top: 8px;
 | 
					 | 
				
			||||||
    gap: 4px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .block-field {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .responsive-hidden {
 | 
					 | 
				
			||||||
    display: unset;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .columns-two {
 | 
					 | 
				
			||||||
    display: grid;
 | 
					 | 
				
			||||||
    grid-template-columns: 1fr 1fr;
 | 
					 | 
				
			||||||
    gap: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (min-width: 768px) {
 | 
					 | 
				
			||||||
    .card-container {
 | 
					 | 
				
			||||||
      grid-template-columns: 1fr 1fr;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .responsive-title-gap {
 | 
					 | 
				
			||||||
      height: calc(56px + 0.44rem);
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@@ -1,43 +0,0 @@
 | 
				
			|||||||
<div class="left-part">
 | 
					 | 
				
			||||||
  <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h1 class="title">{{.i18n.title}}</h1>
 | 
					 | 
				
			||||||
  <p class="caption">{{.i18n.caption}}</p>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <div class="responsive-title-gap"></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <form class="action-form" action="/mfa/apply" method="POST">
 | 
					 | 
				
			||||||
    <label>
 | 
					 | 
				
			||||||
      <input name="ticket_id" value="{{.ticket_id}}" hidden>
 | 
					 | 
				
			||||||
    </label>
 | 
					 | 
				
			||||||
    <label>
 | 
					 | 
				
			||||||
      <input name="factor_id" value="{{.factor_id}}" hidden>
 | 
					 | 
				
			||||||
    </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="factor-label">{{.label}}</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="mb-1 block-field">
 | 
					 | 
				
			||||||
      <label for="code" class="form-label">{{.i18n.password}}</label>
 | 
					 | 
				
			||||||
      <input type="password" class="form-control" id="code" name="password" autocomplete="off">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="action-form-buttons">
 | 
					 | 
				
			||||||
      <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  .factor-label {
 | 
					 | 
				
			||||||
    font-size: 14px;
 | 
					 | 
				
			||||||
    text-align: left;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (min-width: 768px) {
 | 
					 | 
				
			||||||
    .factor-label {
 | 
					 | 
				
			||||||
      text-align: center;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -1,59 +0,0 @@
 | 
				
			|||||||
<div class="left-part">
 | 
					 | 
				
			||||||
  <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h1 class="title">{{.i18n.title}}</h1>
 | 
					 | 
				
			||||||
  <p class="caption">{{.i18n.caption}}</p>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <div class="responsive-title-gap"></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <form class="action-form" action="/mfa" method="POST">
 | 
					 | 
				
			||||||
    <label>
 | 
					 | 
				
			||||||
      <input name="ticket_id" value="{{.ticket_id}}" hidden>
 | 
					 | 
				
			||||||
    </label>
 | 
					 | 
				
			||||||
    {{if ne .redirect_uri nil}}
 | 
					 | 
				
			||||||
    <label>
 | 
					 | 
				
			||||||
      <input name="redirect_uri" value="{{.redirect_uri}}" hidden>
 | 
					 | 
				
			||||||
    </label>
 | 
					 | 
				
			||||||
    {{end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="block-field factor-list" role="radiogroup">
 | 
					 | 
				
			||||||
      {{range $_, $element := .factors}}
 | 
					 | 
				
			||||||
      <div class="factor-label">
 | 
					 | 
				
			||||||
        <div class="form-check">
 | 
					 | 
				
			||||||
          <input class="form-check-input" type="radio" name="factor_id" id="factor-{{$element.id}}"
 | 
					 | 
				
			||||||
            value="{{$element.id}}">
 | 
					 | 
				
			||||||
          <label class="form-check-label" for="factor-{{$element.id}}">
 | 
					 | 
				
			||||||
            {{$element.name}}
 | 
					 | 
				
			||||||
          </label>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {{end}}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="action-form-buttons">
 | 
					 | 
				
			||||||
      <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  .factor-list {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .factor-label {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .factor-label label {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    place-items: center;
 | 
					 | 
				
			||||||
    gap: 8px;
 | 
					 | 
				
			||||||
    font-family: Roboto, system-ui;
 | 
					 | 
				
			||||||
    color: var(--md-sys-color-on-background);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -1,56 +0,0 @@
 | 
				
			|||||||
<head>
 | 
					 | 
				
			||||||
  <meta charset="UTF-8">
 | 
					 | 
				
			||||||
  <meta name="viewport"
 | 
					 | 
				
			||||||
    content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
 | 
					 | 
				
			||||||
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <link rel="icon" type="image/png" href="/favicon.png">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
 | 
					 | 
				
			||||||
    integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
 | 
					 | 
				
			||||||
    integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
 | 
					 | 
				
			||||||
    crossorigin="anonymous"></script>
 | 
					 | 
				
			||||||
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
 | 
					 | 
				
			||||||
    integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
 | 
					 | 
				
			||||||
    crossorigin="anonymous"></script>
 | 
					 | 
				
			||||||
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
 | 
					 | 
				
			||||||
    integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <svg xmlns="http://www.w3.org/2000/svg" class="d-none">
 | 
					 | 
				
			||||||
    <symbol id="info-fill" viewBox="0 0 16 16">
 | 
					 | 
				
			||||||
      <path
 | 
					 | 
				
			||||||
        d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" />
 | 
					 | 
				
			||||||
    </symbol>
 | 
					 | 
				
			||||||
  </svg>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <title>Solarpass</title>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <style>
 | 
					 | 
				
			||||||
    html,
 | 
					 | 
				
			||||||
    body {
 | 
					 | 
				
			||||||
      padding: 0;
 | 
					 | 
				
			||||||
      margin: 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .alert {
 | 
					 | 
				
			||||||
      padding: 16px 48px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      gap: 8px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .alert .bi {
 | 
					 | 
				
			||||||
      aspect-ratio: 1;
 | 
					 | 
				
			||||||
      width: 16px;
 | 
					 | 
				
			||||||
      fill: var(--bs-alert-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .alert .content {
 | 
					 | 
				
			||||||
      flex-grow: 1;
 | 
					 | 
				
			||||||
      text-transform: capitalize;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  </style>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
@@ -1,27 +0,0 @@
 | 
				
			|||||||
<div class="left-part">
 | 
					 | 
				
			||||||
  <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h1 class="title">{{.i18n.title}}</h1>
 | 
					 | 
				
			||||||
  <p class="caption">{{.i18n.caption}}</p>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <div class="responsive-title-gap"></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <form class="action-form" action="/sign-in" method="POST">
 | 
					 | 
				
			||||||
    <div class="mb-1 block-field">
 | 
					 | 
				
			||||||
      <label for="username" class="form-label">{{.i18n.username}}</label>
 | 
					 | 
				
			||||||
      <input type="text" class="form-control" id="username" name="username">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="mb-1 block-field">
 | 
					 | 
				
			||||||
      <label for="password" class="form-label">{{.i18n.password}}</label>
 | 
					 | 
				
			||||||
      <input type="password" class="form-control" id="password" name="password">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="action-form-buttons">
 | 
					 | 
				
			||||||
      <a class="btn btn-secondary" href="/sign-up">{{.i18n.signup}}</a>
 | 
					 | 
				
			||||||
      <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@@ -1,47 +0,0 @@
 | 
				
			|||||||
<div class="left-part">
 | 
					 | 
				
			||||||
  <img class="logo" alt="Logo" src="/favicon.png" width="64" height="64" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <h1 class="title">{{.i18n.title}}</h1>
 | 
					 | 
				
			||||||
  <p class="caption">{{.i18n.caption}}</p>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <div class="responsive-title-gap"></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <form class="action-form" action="/sign-up" method="POST">
 | 
					 | 
				
			||||||
    <div class="columns-two">
 | 
					 | 
				
			||||||
      <div class="mb-1">
 | 
					 | 
				
			||||||
        <label for="name" class="form-label">{{.i18n.username}}</label>
 | 
					 | 
				
			||||||
        <input type="text" class="form-control" id="name" name="name">
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="mb-1">
 | 
					 | 
				
			||||||
        <label for="nick" class="form-label">{{.i18n.nickname}}</label>
 | 
					 | 
				
			||||||
        <input type="text" class="form-control" id="nick" name="nick">
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="mb-1 block-field">
 | 
					 | 
				
			||||||
      <label for="email" class="form-label">{{.i18n.email}}</label>
 | 
					 | 
				
			||||||
      <input type="email" class="form-control" id="email" name="email">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="mb-1">
 | 
					 | 
				
			||||||
      <label for="password" class="form-label">{{.i18n.password}}</label>
 | 
					 | 
				
			||||||
      <input type="password" class="form-control" id="password" name="password" autocomplete="new-password">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {{if eq .use_magic_token true}}
 | 
					 | 
				
			||||||
    <div class="mb-1">
 | 
					 | 
				
			||||||
      <label for="token" class="form-label">{{.i18n.password}}</label>
 | 
					 | 
				
			||||||
      <input type="password" class="form-control" id="token" name="magic_token" autocomplete="new-password">
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    {{end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="action-form-buttons">
 | 
					 | 
				
			||||||
      <a class="btn btn-secondary" href="/sign-in">{{.i18n.signin}}</a>
 | 
					 | 
				
			||||||
      <button class="btn btn-primary" type="submit">{{.i18n.next}}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@@ -1,153 +0,0 @@
 | 
				
			|||||||
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/base.min.css">
 | 
					 | 
				
			||||||
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/components.min.css">
 | 
					 | 
				
			||||||
<link rel="stylesheet" href="https://unpkg.com/@tailwindcss/typography@0.1.2/dist/typography.min.css">
 | 
					 | 
				
			||||||
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@1.4.6/dist/utilities.min.css">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="banner-container">
 | 
					 | 
				
			||||||
  {{if ne .userinfo.Banner nil}}
 | 
					 | 
				
			||||||
  <img src="{{.banner}}" alt="Banner" class="banner">
 | 
					 | 
				
			||||||
  {{end}}
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="left-part name-card">
 | 
					 | 
				
			||||||
  {{if ne .userinfo.Avatar nil}}
 | 
					 | 
				
			||||||
  <img src="{{.avatar}}" alt="Avatar" class="avatar">
 | 
					 | 
				
			||||||
  {{else}}
 | 
					 | 
				
			||||||
  <div class="avatar empty">
 | 
					 | 
				
			||||||
    <span class="material-symbols-outlined">account_circle</span>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  {{end}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div class="name">
 | 
					 | 
				
			||||||
    <h2 class="username">{{.userinfo.Nick}}</h2>
 | 
					 | 
				
			||||||
    <h6 class="nickname">@{{.userinfo.Name}}</h6>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  {{if gt (len .userinfo.Description) 0}}
 | 
					 | 
				
			||||||
  <div class="description">{{.userinfo.Description}}</div>
 | 
					 | 
				
			||||||
  {{else}}
 | 
					 | 
				
			||||||
  <div class="description empty">No description yet.</div>
 | 
					 | 
				
			||||||
  {{end}}
 | 
					 | 
				
			||||||
  <div class="uid">#{{.uid}}</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="right-part">
 | 
					 | 
				
			||||||
  <article class="personal-page prose">
 | 
					 | 
				
			||||||
    {{.personal_page}}
 | 
					 | 
				
			||||||
  </article>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
  .avatar {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    width: 64px;
 | 
					 | 
				
			||||||
    height: 64px;
 | 
					 | 
				
			||||||
    object-fit: cover;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    clip-path: circle();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .avatar.empty {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    background-color: var(--md-sys-color-secondary);
 | 
					 | 
				
			||||||
    color: var(--md-sys-color-on-secondary);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (min-width: 768px) {
 | 
					 | 
				
			||||||
    .banner-container {
 | 
					 | 
				
			||||||
      grid-column: span 2;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .banner {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    object-fit: cover;
 | 
					 | 
				
			||||||
    border-radius: 28px;
 | 
					 | 
				
			||||||
    aspect-ratio: 3 / 1;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .name {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: baseline;
 | 
					 | 
				
			||||||
    gap: 0.3rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .username {
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    font-size: 1.5rem;
 | 
					 | 
				
			||||||
    font-weight: 600;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .nickname {
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    font-size: 0.75rem;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .uid {
 | 
					 | 
				
			||||||
    margin-top: -0.8rem;
 | 
					 | 
				
			||||||
    font-size: 0.7rem;
 | 
					 | 
				
			||||||
    font-weight: 400;
 | 
					 | 
				
			||||||
    font-family: Roboto Mono, monospace;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .description {
 | 
					 | 
				
			||||||
    margin-top: -1.25rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .description.empty {
 | 
					 | 
				
			||||||
    font-style: italic;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .name-card .metadata {
 | 
					 | 
				
			||||||
    font-size: 0.85rem;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .metadata>div {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    gap: 0.25rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .metadata .material-symbols-outlined {
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .actions {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    gap: 0.5rem;
 | 
					 | 
				
			||||||
    margin: 0 -0.5rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media (min-width: 768px) {
 | 
					 | 
				
			||||||
    .actions {
 | 
					 | 
				
			||||||
      flex-direction: column;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .actions .action {
 | 
					 | 
				
			||||||
    width: fit-content;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .actions .material-symbols-outlined {
 | 
					 | 
				
			||||||
    font-size: 20px;
 | 
					 | 
				
			||||||
    margin-bottom: 4px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .left-part .prose {
 | 
					 | 
				
			||||||
    min-width: 0;
 | 
					 | 
				
			||||||
    max-width: unset;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -58,18 +58,17 @@ func main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Server
 | 
						// Server
 | 
				
			||||||
	server.NewServer()
 | 
						go server.NewServer().Listen()
 | 
				
			||||||
	go server.Listen()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Grpc Server
 | 
						// Grpc Server
 | 
				
			||||||
	grpc.NewGRPC()
 | 
						go grpc.NewServer().Listen()
 | 
				
			||||||
	go grpc.ListenGRPC()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Configure timed tasks
 | 
						// Configure timed tasks
 | 
				
			||||||
	quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
 | 
						quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
 | 
				
			||||||
	quartz.AddFunc("@every 60m", services.DoAutoSignoff)
 | 
						quartz.AddFunc("@every 60m", services.DoAutoSignoff)
 | 
				
			||||||
	quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
 | 
						quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
 | 
				
			||||||
	quartz.AddFunc("@every 60s", services.RecycleAuthContext)
 | 
						quartz.AddFunc("@every 60s", services.RecycleAuthContext)
 | 
				
			||||||
 | 
						quartz.AddFunc("@every 60m", services.RecycleUnConfirmAccount)
 | 
				
			||||||
	quartz.AddFunc("@every 5m", services.KexCleanup)
 | 
						quartz.AddFunc("@every 5m", services.KexCleanup)
 | 
				
			||||||
	quartz.Start()
 | 
						quartz.Start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
id = "passport01"
 | 
					id = "passport01"
 | 
				
			||||||
 | 
					name = "Solarpass"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					frontend_app = "web/dist"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bind = "0.0.0.0:8444"
 | 
					bind = "0.0.0.0:8444"
 | 
				
			||||||
grpc_bind = "0.0.0.0:7444"
 | 
					grpc_bind = "0.0.0.0:7444"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								web/.eslintrc.cjs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										18
									
								
								web/.eslintrc.cjs
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					/* eslint-env node */
 | 
				
			||||||
 | 
					require("@rushstack/eslint-patch/modern-module-resolution")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  root: true,
 | 
				
			||||||
 | 
					  extends: [
 | 
				
			||||||
 | 
					    "plugin:vue/vue3-essential",
 | 
				
			||||||
 | 
					    "eslint:recommended",
 | 
				
			||||||
 | 
					    "@vue/eslint-config-typescript",
 | 
				
			||||||
 | 
					    "@vue/eslint-config-prettier/skip-formatting",
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  parserOptions: {
 | 
				
			||||||
 | 
					    ecmaVersion: "latest",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  rules: {
 | 
				
			||||||
 | 
					    "vue/multi-word-component-names": "off",
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					pnpm-debug.log*
 | 
				
			||||||
 | 
					lerna-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					dist-ssr
 | 
				
			||||||
 | 
					coverage
 | 
				
			||||||
 | 
					*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/cypress/videos/
 | 
				
			||||||
 | 
					/cypress/screenshots/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor directories and files
 | 
				
			||||||
 | 
					.vscode/*
 | 
				
			||||||
 | 
					!.vscode/extensions.json
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
 | 
					*.suo
 | 
				
			||||||
 | 
					*.ntvs*
 | 
				
			||||||
 | 
					*.njsproj
 | 
				
			||||||
 | 
					*.sln
 | 
				
			||||||
 | 
					*.sw?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.tsbuildinfo
 | 
				
			||||||
							
								
								
									
										8
									
								
								web/.prettierrc.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										8
									
								
								web/.prettierrc.json
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "https://json.schemastore.org/prettierrc",
 | 
				
			||||||
 | 
					  "semi": false,
 | 
				
			||||||
 | 
					  "tabWidth": 2,
 | 
				
			||||||
 | 
					  "singleQuote": false,
 | 
				
			||||||
 | 
					  "printWidth": 120,
 | 
				
			||||||
 | 
					  "trailingComma": "all"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								web/README.md
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										39
									
								
								web/README.md
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					# views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This template should help get you started developing with Vue 3 in Vite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Recommended IDE Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Type Support for `.vue` Imports in TS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Customize configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See [Vite Configuration Reference](https://vitejs.dev/config/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Project Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					npm install
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Compile and Hot-Reload for Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					npm run dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Type-Check, Compile and Minify for Production
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					npm run build
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Lint with [ESLint](https://eslint.org/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					npm run lint
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								web/env.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								web/env.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/index.html
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								web/index.html
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/xml+svg" href="/favicon.png" />
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
 | 
					    <title>Solarpass</title>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="app"></div>
 | 
				
			||||||
 | 
					    <script type="module" src="/src/main.ts"></script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										45
									
								
								web/package.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										45
									
								
								web/package.json
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "passport-web",
 | 
				
			||||||
 | 
					  "version": "1.0.0",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "dev": "vite",
 | 
				
			||||||
 | 
					    "build": "run-p type-check \"build-only {@}\" --",
 | 
				
			||||||
 | 
					    "preview": "vite preview",
 | 
				
			||||||
 | 
					    "build-only": "vite build",
 | 
				
			||||||
 | 
					    "type-check": "vue-tsc --build --force",
 | 
				
			||||||
 | 
					    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 | 
				
			||||||
 | 
					    "format": "prettier --write src/"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@fontsource/roboto": "^5.0.13",
 | 
				
			||||||
 | 
					    "@mdi/font": "^7.4.47",
 | 
				
			||||||
 | 
					    "@unocss/reset": "^0.58.9",
 | 
				
			||||||
 | 
					    "dompurify": "^3.1.5",
 | 
				
			||||||
 | 
					    "marked": "^12.0.2",
 | 
				
			||||||
 | 
					    "pinia": "^2.1.7",
 | 
				
			||||||
 | 
					    "universal-cookie": "^7.1.4",
 | 
				
			||||||
 | 
					    "unocss": "^0.58.9",
 | 
				
			||||||
 | 
					    "vue": "^3.4.30",
 | 
				
			||||||
 | 
					    "vue-router": "^4.4.0",
 | 
				
			||||||
 | 
					    "vuetify": "^3.6.10"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@rushstack/eslint-patch": "^1.10.3",
 | 
				
			||||||
 | 
					    "@tsconfig/node20": "^20.1.4",
 | 
				
			||||||
 | 
					    "@types/dompurify": "^3.0.5",
 | 
				
			||||||
 | 
					    "@types/node": "^20.14.8",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-vue": "^5.0.5",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-vue-jsx": "^3.1.0",
 | 
				
			||||||
 | 
					    "@vue/eslint-config-prettier": "^8.0.0",
 | 
				
			||||||
 | 
					    "@vue/eslint-config-typescript": "^12.0.0",
 | 
				
			||||||
 | 
					    "@vue/tsconfig": "^0.5.1",
 | 
				
			||||||
 | 
					    "eslint": "^8.57.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-vue": "^9.26.0",
 | 
				
			||||||
 | 
					    "npm-run-all2": "^6.2.0",
 | 
				
			||||||
 | 
					    "prettier": "^3.3.2",
 | 
				
			||||||
 | 
					    "typescript": "^5.4.5",
 | 
				
			||||||
 | 
					    "vite": "^5.3.1",
 | 
				
			||||||
 | 
					    "vue-tsc": "^2.0.22"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 75 KiB  | 
							
								
								
									
										14
									
								
								web/src/assets/utils.css
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								web/src/assets/utils.css
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					html,
 | 
				
			||||||
 | 
					body,
 | 
				
			||||||
 | 
					#app,
 | 
				
			||||||
 | 
					.v-application {
 | 
				
			||||||
 | 
					  font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.no-scrollbar {
 | 
				
			||||||
 | 
					  scrollbar-width: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.no-scrollbar::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								web/src/components/Copyright.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								web/src/components/Copyright.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="text-xs text-center opacity-80">
 | 
				
			||||||
 | 
					    <p>Copyright © {{ new Date().getFullYear() }} Solsynth LLC</p>
 | 
				
			||||||
 | 
					    <p>Powered by <a class="underline" href="https://git.solsynth.dev/Hydrogen/Passport">Hydrogen.Passport</a></p>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										19
									
								
								web/src/components/GoUseSolian.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								web/src/components/GoUseSolian.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-card prepend-icon="mdi-cellphone-arrow-down-variant" title="Try the App">
 | 
				
			||||||
 | 
					    <v-card-text>
 | 
				
			||||||
 | 
					      <p>
 | 
				
			||||||
 | 
					        Some features on Solarpass web was incomplete.
 | 
				
			||||||
 | 
					        Go try out our brand-new all-in-one Solar Network application now!
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </v-card-text>
 | 
				
			||||||
 | 
					    <v-card-actions>
 | 
				
			||||||
 | 
					      <v-btn prepend-icon="mdi-launch" href="https://lian.solsynth.dev" target="_blank">
 | 
				
			||||||
 | 
					        Open in browser
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					      <v-btn prepend-icon="mdi-download" href="https://git.solsynth.dev/Hydrogen/Solian/releases" target="_blank"
 | 
				
			||||||
 | 
					             color="teal">
 | 
				
			||||||
 | 
					        Download now
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					    </v-card-actions>
 | 
				
			||||||
 | 
					  </v-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										68
									
								
								web/src/components/NotificationList.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								web/src/components/NotificationList.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-navigation-drawer :model-value="props.open" @update:model-value="val => emits('update:open', val)" location="right"
 | 
				
			||||||
 | 
					                       temporary order="0" width="400">
 | 
				
			||||||
 | 
					    <v-list-item prepend-icon="mdi-bell" title="Notifications" class="py-3"></v-list-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-divider color="black" class="mb-1" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-list v-if="notify.notifications.length <= 0" density="compact">
 | 
				
			||||||
 | 
					      <v-list-item color="secondary" prepend-icon="mdi-check" title="All notifications read"
 | 
				
			||||||
 | 
					                   subtitle="There is no more new things for you..." />
 | 
				
			||||||
 | 
					    </v-list>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-list v-else class="w-[380px]" density="compact" lines="three">
 | 
				
			||||||
 | 
					      <v-list-item v-for="(item, idx) in notify.notifications" :key="idx">
 | 
				
			||||||
 | 
					        <template #title>{{ item.subject }}</template>
 | 
				
			||||||
 | 
					        <template #subtitle>{{ item.content }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #append>
 | 
				
			||||||
 | 
					          <v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item, idx)" />
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="flex text-xs gap-1">
 | 
				
			||||||
 | 
					          <a v-for="(link, idx) in item.links" :key="idx" class="mt-1 underline" target="_blank"
 | 
				
			||||||
 | 
					             :href="link.url">{{ link.label }}</a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </v-list-item>
 | 
				
			||||||
 | 
					    </v-list>
 | 
				
			||||||
 | 
					  </v-navigation-drawer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- @vue-ignore -->
 | 
				
			||||||
 | 
					  <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { getAtk } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { computed, onMounted, onUnmounted, ref } from "vue"
 | 
				
			||||||
 | 
					import { useNotifications } from "@/stores/notifications"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{ open: boolean }>()
 | 
				
			||||||
 | 
					const emits = defineEmits(["update:open"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const notify = useNotifications()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					const submitting = ref(false)
 | 
				
			||||||
 | 
					const loading = computed(() => notify.loading || submitting.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function markAsRead(item: any, idx: number) {
 | 
				
			||||||
 | 
					  submitting.value = true
 | 
				
			||||||
 | 
					  const res = await request(`/api/notifications/${item.id}/read`, {
 | 
				
			||||||
 | 
					    method: "PUT",
 | 
				
			||||||
 | 
					    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    notify.remove(idx)
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  submitting.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					notify.list()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => notify.connect())
 | 
				
			||||||
 | 
					onUnmounted(() => notify.disconnect())
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										53
									
								
								web/src/components/UserMenu.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										53
									
								
								web/src/components/UserMenu.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-menu>
 | 
				
			||||||
 | 
					    <template #activator="{ props }">
 | 
				
			||||||
 | 
					      <v-btn flat exact v-bind="props" icon>
 | 
				
			||||||
 | 
					        <v-avatar color="transparent" icon="mdi-account-circle" :image="'/api/avatar/' + id.userinfo.data?.avatar" />
 | 
				
			||||||
 | 
					      </v-btn>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-list density="compact" v-if="!id.userinfo.isLoggedIn">
 | 
				
			||||||
 | 
					      <v-list-item title="Sign in" prepend-icon="mdi-login-variant" :to="{ name: 'auth.sign-in' }" />
 | 
				
			||||||
 | 
					      <v-list-item title="Create account" prepend-icon="mdi-account-plus" :to="{ name: 'auth.sign-up' }" />
 | 
				
			||||||
 | 
					    </v-list>
 | 
				
			||||||
 | 
					    <v-list density="compact" v-else>
 | 
				
			||||||
 | 
					      <v-list-item :title="nickname" :subtitle="username" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-divider class="border-opacity-50 my-2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-list-item title="Dashboard" prepend-icon="mdi-account-supervisor" exact :to="{ name: 'dashboard' }" />
 | 
				
			||||||
 | 
					      <v-list-item title="Sign out" prepend-icon="mdi-logout" @click="signout"></v-list-item>
 | 
				
			||||||
 | 
					    </v-list>
 | 
				
			||||||
 | 
					  </v-menu>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { defaultUserinfo, useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { computed } from "vue"
 | 
				
			||||||
 | 
					import Cookie from "universal-cookie"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const id = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const username = computed(() => {
 | 
				
			||||||
 | 
					  if (id.userinfo.isLoggedIn) {
 | 
				
			||||||
 | 
					    return "@" + id.userinfo.data?.name
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return "@visitor"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					const nickname = computed(() => {
 | 
				
			||||||
 | 
					  if (id.userinfo.isLoggedIn) {
 | 
				
			||||||
 | 
					    return id.userinfo.data?.nick
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return "Anonymous"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function signout() {
 | 
				
			||||||
 | 
					  const ck = new Cookie();
 | 
				
			||||||
 | 
					  ck.remove("__hydrogen_atk");
 | 
				
			||||||
 | 
					  ck.remove("__hydrogen_rtk")
 | 
				
			||||||
 | 
					  id.userinfo = defaultUserinfo
 | 
				
			||||||
 | 
					  window.location.reload()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										65
									
								
								web/src/components/auth/Authenticate.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										65
									
								
								web/src/components/auth/Authenticate.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex items-center">
 | 
				
			||||||
 | 
					    <v-form class="flex-grow-1" @submit.prevent="submit">
 | 
				
			||||||
 | 
					      <v-text-field label="Username" variant="solo" density="comfortable" class="mb-3" :hide-details="true"
 | 
				
			||||||
 | 
					                    :disabled="props.loading" v-model="probe" />
 | 
				
			||||||
 | 
					      <v-text-field label="Password" variant="solo" density="comfortable" type="password" :disabled="props.loading"
 | 
				
			||||||
 | 
					                    v-model="password" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-expand-transition>
 | 
				
			||||||
 | 
					        <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					          Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					        </v-alert>
 | 
				
			||||||
 | 
					      </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex justify-between">
 | 
				
			||||||
 | 
					        <v-btn type="button" variant="plain" color="grey-darken-3" :to="{ name: 'auth.sign-up' }">Sign up</v-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <v-btn
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
 | 
					          variant="text"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          class="justify-self-end"
 | 
				
			||||||
 | 
					          append-icon="mdi-arrow-right"
 | 
				
			||||||
 | 
					          :disabled="props.loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Next
 | 
				
			||||||
 | 
					        </v-btn>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </v-form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const probe = ref("")
 | 
				
			||||||
 | 
					const password = ref("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{ loading?: boolean }>()
 | 
				
			||||||
 | 
					const emits = defineEmits(["swap", "update:loading", "update:ticket"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  if (!probe.value || !password.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  emits("update:loading", true)
 | 
				
			||||||
 | 
					  const res = await request("/api/auth", {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify({ id: probe.value, password: password.value }),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					    emits("update:ticket", data["ticket"])
 | 
				
			||||||
 | 
					    if (data.is_finished) emits("swap", "completed")
 | 
				
			||||||
 | 
					    else emits("swap", "mfa")
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  emits("update:loading", false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										68
									
								
								web/src/components/auth/AuthenticateCompleted.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								web/src/components/auth/AuthenticateCompleted.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-icon icon="mdi-lan-check" size="32" color="grey-darken-3" class="mb-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h1 class="font-bold text-xl">All Done!</h1>
 | 
				
			||||||
 | 
					    <p>Welcome back! You just signed in right now! We're going to send you to jesus...</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-expand-transition>
 | 
				
			||||||
 | 
					      <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					        Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					      </v-alert>
 | 
				
			||||||
 | 
					    </v-expand-transition>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { onMounted, ref } from "vue"
 | 
				
			||||||
 | 
					import { useRoute, useRouter } from "vue-router"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const userinfo = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>()
 | 
				
			||||||
 | 
					const emits = defineEmits(["update:loading"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function load() {
 | 
				
			||||||
 | 
					  emits("update:loading", true)
 | 
				
			||||||
 | 
					  await getToken(props.ticket.grant_token)
 | 
				
			||||||
 | 
					  await userinfo.readProfiles()
 | 
				
			||||||
 | 
					  emits("update:loading", false)
 | 
				
			||||||
 | 
					  setTimeout(() => callback(), 3000)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => load())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getToken(tk: string) {
 | 
				
			||||||
 | 
					  const res = await request("/api/auth/token", {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify({
 | 
				
			||||||
 | 
					      code: tk,
 | 
				
			||||||
 | 
					      grant_type: "grant_token",
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    const err = await res.text()
 | 
				
			||||||
 | 
					    error.value = err
 | 
				
			||||||
 | 
					    throw new Error(err)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function callback() {
 | 
				
			||||||
 | 
					  if (route.query["close"]) {
 | 
				
			||||||
 | 
					    window.close()
 | 
				
			||||||
 | 
					  } else if (route.query["redirect_uri"]) {
 | 
				
			||||||
 | 
					    window.open((route.query["redirect_uri"] as string) ?? "/", "_self")
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    router.push({ name: "dashboard" })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										16
									
								
								web/src/components/auth/CallbackNotify.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										16
									
								
								web/src/components/auth/CallbackNotify.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="w-full max-w-[720px]">
 | 
				
			||||||
 | 
					    <v-expand-transition>
 | 
				
			||||||
 | 
					      <v-alert v-show="route.query['redirect_uri']" variant="tonal" type="info" class="text-xs">
 | 
				
			||||||
 | 
					        You need to sign in before access that page. After you signed in, we will redirect you to: <br />
 | 
				
			||||||
 | 
					        <span class="font-mono">{{ route.query["redirect_uri"] }}</span>
 | 
				
			||||||
 | 
					      </v-alert>
 | 
				
			||||||
 | 
					    </v-expand-transition>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useRoute } from "vue-router"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										93
									
								
								web/src/components/auth/FactorApplicator.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										93
									
								
								web/src/components/auth/FactorApplicator.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex items-center">
 | 
				
			||||||
 | 
					    <v-form class="flex-grow-1" @submit.prevent="submit">
 | 
				
			||||||
 | 
					      <div v-if="inputType === 'one-time-password'" class="text-center">
 | 
				
			||||||
 | 
					        <p class="text-xs opacity-90">Check your inbox!</p>
 | 
				
			||||||
 | 
					        <v-otp-input
 | 
				
			||||||
 | 
					          class="pt-0"
 | 
				
			||||||
 | 
					          variant="solo"
 | 
				
			||||||
 | 
					          density="compact"
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          :length="6"
 | 
				
			||||||
 | 
					          v-model="password"
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <v-text-field
 | 
				
			||||||
 | 
					        v-else
 | 
				
			||||||
 | 
					        label="Password"
 | 
				
			||||||
 | 
					        type="password"
 | 
				
			||||||
 | 
					        variant="solo"
 | 
				
			||||||
 | 
					        density="comfortable"
 | 
				
			||||||
 | 
					        :disabled="loading"
 | 
				
			||||||
 | 
					        v-model="password"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-expand-transition>
 | 
				
			||||||
 | 
					        <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					          Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					        </v-alert>
 | 
				
			||||||
 | 
					      </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex justify-end">
 | 
				
			||||||
 | 
					        <v-btn
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
 | 
					          variant="text"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          class="justify-self-end"
 | 
				
			||||||
 | 
					          append-icon="mdi-arrow-right"
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Next
 | 
				
			||||||
 | 
					        </v-btn>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </v-form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { computed, ref } from "vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const password = ref("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>()
 | 
				
			||||||
 | 
					const emits = defineEmits(["swap", "update:ticket", "update:loading"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  emits("update:loading", true)
 | 
				
			||||||
 | 
					  const res = await request(`/api/auth/mfa`, {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify({
 | 
				
			||||||
 | 
					      ticket_id: props.ticket?.id,
 | 
				
			||||||
 | 
					      factor_id: props.currentFactor?.id,
 | 
				
			||||||
 | 
					      code: password.value,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					    password.value = ""
 | 
				
			||||||
 | 
					    emits("update:ticket", data["ticket"])
 | 
				
			||||||
 | 
					    if (data["is_finished"]) emits("swap", "completed")
 | 
				
			||||||
 | 
					    else emits("swap", "mfa")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  emits("update:loading", false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const inputType = computed(() => {
 | 
				
			||||||
 | 
					  switch (props.currentFactor?.type) {
 | 
				
			||||||
 | 
					    case 0:
 | 
				
			||||||
 | 
					      return "text"
 | 
				
			||||||
 | 
					    case 1:
 | 
				
			||||||
 | 
					      return "one-time-password"
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return "unknown"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										88
									
								
								web/src/components/auth/FactorPicker.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										88
									
								
								web/src/components/auth/FactorPicker.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex items-center">
 | 
				
			||||||
 | 
					    <div class="flex-grow-1">
 | 
				
			||||||
 | 
					      <v-card class="mb-3">
 | 
				
			||||||
 | 
					        <v-list density="compact" color="primary">
 | 
				
			||||||
 | 
					          <v-list-item
 | 
				
			||||||
 | 
					            v-for="(item, idx) in factors ?? []"
 | 
				
			||||||
 | 
					            :key="idx"
 | 
				
			||||||
 | 
					            :prepend-icon="getFactorType(item)?.icon"
 | 
				
			||||||
 | 
					            :title="getFactorType(item)?.label"
 | 
				
			||||||
 | 
					            :active="focus === item.id"
 | 
				
			||||||
 | 
					            :disabled="getFactorAvailable(item)"
 | 
				
			||||||
 | 
					            @click="focus = item.id"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </v-list>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-expand-transition>
 | 
				
			||||||
 | 
					        <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					          Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					        </v-alert>
 | 
				
			||||||
 | 
					      </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex justify-end">
 | 
				
			||||||
 | 
					        <v-btn variant="text" color="primary" class="justify-self-end" append-icon="mdi-arrow-right" @click="submit">
 | 
				
			||||||
 | 
					          Next
 | 
				
			||||||
 | 
					        </v-btn>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { onMounted, ref } from "vue"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const focus = ref<number | null>(null)
 | 
				
			||||||
 | 
					const factors = ref<any[]>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{ ticket?: any }>()
 | 
				
			||||||
 | 
					const emits = defineEmits(["swap", "update:loading", "update:currentFactor"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function load() {
 | 
				
			||||||
 | 
					  emits("update:loading", true)
 | 
				
			||||||
 | 
					  const res = await request(`/api/auth/factors?ticketId=${props.ticket.id}`)
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    factors.value = (await res.json()).filter((e: any) => e.type != 0)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  emits("update:loading", false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => load())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  if (!focus.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  emits("update:loading", true)
 | 
				
			||||||
 | 
					  const res = await request(`/api/auth/factors/${focus.value}`, {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200 && res.status !== 204) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const item = factors.value.find((item: any) => item.id === focus.value)
 | 
				
			||||||
 | 
					    emits("update:currentFactor", item)
 | 
				
			||||||
 | 
					    emits("swap", "applicator")
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					    focus.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  emits("update:loading", false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getFactorType(item: any) {
 | 
				
			||||||
 | 
					  switch (item.type) {
 | 
				
			||||||
 | 
					    case 1:
 | 
				
			||||||
 | 
					      return { icon: "mdi-email-fast", label: "Email Validation" }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getFactorAvailable(factor: any) {
 | 
				
			||||||
 | 
					  const blacklist: number[] = props.ticket?.blacklist_factors ?? []
 | 
				
			||||||
 | 
					  return blacklist.includes(factor.id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										49
									
								
								web/src/components/navigation/AppBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/components/navigation/AppBar.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
 | 
				
			||||||
 | 
					    <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
 | 
				
			||||||
 | 
					      <router-link :to="{ name: 'dashboard' }" class="flex gap-1 ms-0.5">
 | 
				
			||||||
 | 
					        <img src="/favicon.png" alt="logo" width="27" height="24" class="icon-filter" />
 | 
				
			||||||
 | 
					        <h2 class="ml-2 text-lg font-500">Solarpass</h2>
 | 
				
			||||||
 | 
					      </router-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-spacer />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="me-2">
 | 
				
			||||||
 | 
					        <v-btn icon size="small" variant="text" @click="openNotify = !openNotify">
 | 
				
			||||||
 | 
					          <v-badge v-if="notify.total > 0" color="error" :content="notify.total">
 | 
				
			||||||
 | 
					            <v-icon icon="mdi-bell" />
 | 
				
			||||||
 | 
					          </v-badge>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-icon v-else icon="mdi-bell" />
 | 
				
			||||||
 | 
					        </v-btn>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <user-menu />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <template #extension>
 | 
				
			||||||
 | 
					      <slot name="extension" />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					  </v-app-bar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <NotificationList v-model:open="openNotify" />
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import NotificationList from "@/components/NotificationList.vue"
 | 
				
			||||||
 | 
					import UserMenu from "@/components/UserMenu.vue"
 | 
				
			||||||
 | 
					import { useNotifications } from "@/stores/notifications"
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const notify = useNotifications()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openNotify = ref(false)
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.icon-filter {
 | 
				
			||||||
 | 
					  filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										5
									
								
								web/src/index.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								web/src/index.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-app>
 | 
				
			||||||
 | 
					    <router-view />
 | 
				
			||||||
 | 
					  </v-app>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										22
									
								
								web/src/layouts/master.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										22
									
								
								web/src/layouts/master.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <AppBar />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <v-main>
 | 
				
			||||||
 | 
					    <router-view />
 | 
				
			||||||
 | 
					  </v-main>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import AppBar from "@/components/navigation/AppBar.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const id = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id.readProfiles()
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.icon-filter {
 | 
				
			||||||
 | 
					  filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										30
									
								
								web/src/layouts/user-center.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								web/src/layouts/user-center.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <AppBar>
 | 
				
			||||||
 | 
					    <template #extension>
 | 
				
			||||||
 | 
					      <v-tabs align-tabs="title" color="white">
 | 
				
			||||||
 | 
					        <v-tab text="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact />
 | 
				
			||||||
 | 
					        <v-tab text="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" exact />
 | 
				
			||||||
 | 
					        <v-tab text="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" exact />
 | 
				
			||||||
 | 
					      </v-tabs>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					  </AppBar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <v-main>
 | 
				
			||||||
 | 
					    <v-container class="pt-6 px-6 p-container">
 | 
				
			||||||
 | 
					      <router-view />
 | 
				
			||||||
 | 
					    </v-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <Copyright />
 | 
				
			||||||
 | 
					  </v-main>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import AppBar from "@/components/navigation/AppBar.vue"
 | 
				
			||||||
 | 
					import Copyright from "@/components/Copyright.vue"
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.p-container {
 | 
				
			||||||
 | 
					  max-width: 40rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										54
									
								
								web/src/main.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										54
									
								
								web/src/main.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import "virtual:uno.css"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "./assets/utils.css"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createApp } from "vue"
 | 
				
			||||||
 | 
					import { createPinia } from "pinia"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "vuetify/styles"
 | 
				
			||||||
 | 
					import { createVuetify } from "vuetify"
 | 
				
			||||||
 | 
					import { md3 } from "vuetify/blueprints"
 | 
				
			||||||
 | 
					import * as components from "vuetify/components"
 | 
				
			||||||
 | 
					import * as labsComponents from "vuetify/labs/components"
 | 
				
			||||||
 | 
					import * as directives from "vuetify/directives"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "@mdi/font/css/materialdesignicons.min.css"
 | 
				
			||||||
 | 
					import "@fontsource/roboto/latin.css"
 | 
				
			||||||
 | 
					import "@unocss/reset/tailwind.css"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import index from "./index.vue"
 | 
				
			||||||
 | 
					import router from "./router"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const app = createApp(index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use(
 | 
				
			||||||
 | 
					  createVuetify({
 | 
				
			||||||
 | 
					    directives,
 | 
				
			||||||
 | 
					    components: {
 | 
				
			||||||
 | 
					      ...components,
 | 
				
			||||||
 | 
					      ...labsComponents,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    blueprint: md3,
 | 
				
			||||||
 | 
					    theme: {
 | 
				
			||||||
 | 
					      defaultTheme: "original",
 | 
				
			||||||
 | 
					      themes: {
 | 
				
			||||||
 | 
					        original: {
 | 
				
			||||||
 | 
					          colors: {
 | 
				
			||||||
 | 
					            primary: "#4a5099",
 | 
				
			||||||
 | 
					            secondary: "#2196f3",
 | 
				
			||||||
 | 
					            accent: "#009688",
 | 
				
			||||||
 | 
					            error: "#f44336",
 | 
				
			||||||
 | 
					            warning: "#ff9800",
 | 
				
			||||||
 | 
					            info: "#03a9f4",
 | 
				
			||||||
 | 
					            success: "#4caf50",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use(createPinia())
 | 
				
			||||||
 | 
					app.use(router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.mount("#app")
 | 
				
			||||||
							
								
								
									
										87
									
								
								web/src/router/index.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										87
									
								
								web/src/router/index.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					import { createRouter, createWebHistory } from "vue-router"
 | 
				
			||||||
 | 
					import { useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import UserCenterLayout from "@/layouts/user-center.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = createRouter({
 | 
				
			||||||
 | 
					  history: createWebHistory(import.meta.env.BASE_URL),
 | 
				
			||||||
 | 
					  routes: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: "/",
 | 
				
			||||||
 | 
					      redirect: { name: "dashboard" },
 | 
				
			||||||
 | 
					      meta: { public: true },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: "/users",
 | 
				
			||||||
 | 
					      component: UserCenterLayout,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "/me",
 | 
				
			||||||
 | 
					          name: "dashboard",
 | 
				
			||||||
 | 
					          component: () => import("@/views/dashboard.vue"),
 | 
				
			||||||
 | 
					          meta: { title: "Your account" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "/me/personalize",
 | 
				
			||||||
 | 
					          name: "personalize",
 | 
				
			||||||
 | 
					          component: () => import("@/views/personalize.vue"),
 | 
				
			||||||
 | 
					          meta: { title: "Your personality" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "/me/security",
 | 
				
			||||||
 | 
					          name: "security",
 | 
				
			||||||
 | 
					          component: () => import("@/views/security.vue"),
 | 
				
			||||||
 | 
					          meta: { title: "Your security" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: "/",
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "/sign-in",
 | 
				
			||||||
 | 
					          name: "auth.sign-in",
 | 
				
			||||||
 | 
					          component: () => import("@/views/auth/sign-in.vue"),
 | 
				
			||||||
 | 
					          meta: { public: true, title: "Sign in" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "/sign-up",
 | 
				
			||||||
 | 
					          name: "auth.sign-up",
 | 
				
			||||||
 | 
					          component: () => import("@/views/auth/sign-up.vue"),
 | 
				
			||||||
 | 
					          meta: { public: true, title: "Sign up" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "authorize",
 | 
				
			||||||
 | 
					          name: "oauth.authorize",
 | 
				
			||||||
 | 
					          component: () => import("@/views/auth/authorize.vue"),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: "/users/me/confirm",
 | 
				
			||||||
 | 
					      name: "callback.confirm",
 | 
				
			||||||
 | 
					      component: () => import("@/views/confirm.vue"),
 | 
				
			||||||
 | 
					      meta: { public: true, title: "Confirm registration" },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.beforeEach(async (to, from, next) => {
 | 
				
			||||||
 | 
					  const id = useUserinfo()
 | 
				
			||||||
 | 
					  if (!id.isReady) {
 | 
				
			||||||
 | 
					    await id.readProfiles()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (to.meta.title) {
 | 
				
			||||||
 | 
					    document.title = `Solarpass | ${to.meta.title}`
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    document.title = "Solarpass"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!to.meta.public && !id.userinfo.isLoggedIn) {
 | 
				
			||||||
 | 
					    next({ name: "auth.sign-in", query: { redirect_uri: to.fullPath } })
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    next()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default router
 | 
				
			||||||
							
								
								
									
										3
									
								
								web/src/scripts/request.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								web/src/scripts/request.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					export async function request(input: string, init?: RequestInit) {
 | 
				
			||||||
 | 
					  return await fetch(input, init)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								web/src/stores/notifications.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								web/src/stores/notifications.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					import { defineStore } from "pinia"
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					import { checkLoggedIn, getAtk } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useNotifications = defineStore("notifications", () => {
 | 
				
			||||||
 | 
					  let socket: WebSocket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const notifications = ref<any[]>([])
 | 
				
			||||||
 | 
					  const total = ref(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function list() {
 | 
				
			||||||
 | 
					    loading.value = true
 | 
				
			||||||
 | 
					    const res = await request(
 | 
				
			||||||
 | 
					      "/api/notifications?" +
 | 
				
			||||||
 | 
					      new URLSearchParams({
 | 
				
			||||||
 | 
					        take: (25).toString(),
 | 
				
			||||||
 | 
					        offset: (0).toString(),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if (res.status === 200) {
 | 
				
			||||||
 | 
					      const data = await res.json()
 | 
				
			||||||
 | 
					      notifications.value = data["data"]
 | 
				
			||||||
 | 
					      total.value = data["count"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    loading.value = false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function remove(idx: number) {
 | 
				
			||||||
 | 
					    notifications.value.splice(idx, 1)
 | 
				
			||||||
 | 
					    total.value--
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function connect() {
 | 
				
			||||||
 | 
					    if (!(checkLoggedIn())) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const uri = `ws://${window.location.host}/api/ws`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket = new WebSocket(uri + `?tk=${getAtk() as string}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket.addEventListener("open", (event) => {
 | 
				
			||||||
 | 
					      console.log("[NOTIFICATIONS] The listen websocket has been established... ", event.type)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    socket.addEventListener("close", (event) => {
 | 
				
			||||||
 | 
					      console.warn("[NOTIFICATIONS] The listen websocket is disconnected... ", event.reason, event.code)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    socket.addEventListener("message", (event) => {
 | 
				
			||||||
 | 
					      const data = JSON.parse(event.data)
 | 
				
			||||||
 | 
					      if (data["w"] == "notifications.new") {
 | 
				
			||||||
 | 
					        notifications.value.push(data["p"])
 | 
				
			||||||
 | 
					        total.value++
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function disconnect() {
 | 
				
			||||||
 | 
					    socket.close()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { loading, notifications, total, list, remove, connect, disconnect }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										54
									
								
								web/src/stores/userinfo.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										54
									
								
								web/src/stores/userinfo.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import Cookie from "universal-cookie"
 | 
				
			||||||
 | 
					import { defineStore } from "pinia"
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Userinfo {
 | 
				
			||||||
 | 
					  isLoggedIn: boolean
 | 
				
			||||||
 | 
					  displayName: string
 | 
				
			||||||
 | 
					  data: any
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const defaultUserinfo: Userinfo = {
 | 
				
			||||||
 | 
					  isLoggedIn: false,
 | 
				
			||||||
 | 
					  displayName: "Citizen",
 | 
				
			||||||
 | 
					  data: null,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getAtk(): string {
 | 
				
			||||||
 | 
					  return new Cookie().get("__hydrogen_atk")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function checkLoggedIn(): boolean {
 | 
				
			||||||
 | 
					  return new Cookie().get("__hydrogen_rtk")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUserinfo = defineStore("userinfo", () => {
 | 
				
			||||||
 | 
					  const userinfo = ref(defaultUserinfo)
 | 
				
			||||||
 | 
					  const isReady = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function readProfiles() {
 | 
				
			||||||
 | 
					    if (!checkLoggedIn()) {
 | 
				
			||||||
 | 
					      isReady.value = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await request("/api/users/me", {
 | 
				
			||||||
 | 
					      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (res.status !== 200) {
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isReady.value = true
 | 
				
			||||||
 | 
					    userinfo.value = {
 | 
				
			||||||
 | 
					      isLoggedIn: true,
 | 
				
			||||||
 | 
					      displayName: data["nick"],
 | 
				
			||||||
 | 
					      data: data,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { userinfo, isReady, readProfiles }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										196
									
								
								web/src/views/auth/authorize.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										196
									
								
								web/src/views/auth/authorize.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-container class="h-screen flex flex-col gap-3 items-center justify-center">
 | 
				
			||||||
 | 
					    <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
 | 
				
			||||||
 | 
					      <v-card-text class="card-grid pa-9">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <v-avatar color="accent" icon="mdi-connection" size="large" class="card-rounded mb-2" />
 | 
				
			||||||
 | 
					          <h1 class="text-2xl">Connect to third-party</h1>
 | 
				
			||||||
 | 
					          <p>One Solarpass, entire internet.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
 | 
				
			||||||
 | 
					          <v-window-item value="confirm">
 | 
				
			||||||
 | 
					            <div class="flex flex-col gap-2">
 | 
				
			||||||
 | 
					              <v-expand-transition>
 | 
				
			||||||
 | 
					                <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					                  <p>Something went wrong... {{ error }}</p>
 | 
				
			||||||
 | 
					                  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <p class="font-bold">
 | 
				
			||||||
 | 
					                    It's usually not our fault. Try bringing this link to give feedback to the developer of the app you
 | 
				
			||||||
 | 
					                    came from.
 | 
				
			||||||
 | 
					                  </p>
 | 
				
			||||||
 | 
					                </v-alert>
 | 
				
			||||||
 | 
					              </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div v-if="!error">
 | 
				
			||||||
 | 
					                <h1 class="font-bold text-xl">{{ metadata?.name ?? "Loading" }}</h1>
 | 
				
			||||||
 | 
					                <p>{{ metadata?.description ?? "Hold on a second please!" }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="mt-3">
 | 
				
			||||||
 | 
					                  <p class="opacity-80 text-xs">Permissions they requested</p>
 | 
				
			||||||
 | 
					                  <v-card variant="tonal" class="mt-1 mx-[-4px]">
 | 
				
			||||||
 | 
					                    <v-list density="compact">
 | 
				
			||||||
 | 
					                      <v-list-item v-for="(claim, key) in requestedClaims" :key="key" lines="two">
 | 
				
			||||||
 | 
					                        <template #title>
 | 
				
			||||||
 | 
					                          <span class="capitalize">{{ getClaimDescription(claim)?.name }}</span>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                        <template #subtitle>
 | 
				
			||||||
 | 
					                          <span>{{ getClaimDescription(claim)?.description }}</span>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                        <template #prepend>
 | 
				
			||||||
 | 
					                          <v-icon :icon="getClaimDescription(claim)?.icon" size="x-large" />
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                      </v-list-item>
 | 
				
			||||||
 | 
					                    </v-list>
 | 
				
			||||||
 | 
					                  </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div class="mt-5 flex justify-between">
 | 
				
			||||||
 | 
					                    <v-btn prepend-icon="mdi-close" variant="text" color="error" :disabled="loading" @click="decline">
 | 
				
			||||||
 | 
					                      Decline
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                    <v-btn append-icon="mdi-check" variant="tonal" color="success" :disabled="loading" @click="approve">
 | 
				
			||||||
 | 
					                      Approve
 | 
				
			||||||
 | 
					                    </v-btn>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div class="mt-5 text-xs text-center opacity-75">
 | 
				
			||||||
 | 
					                    <p>After approve their request, you will be redirect to</p>
 | 
				
			||||||
 | 
					                    <p class="text-mono">{{ route.query["redirect_uri"] }}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-window-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-window-item value="callback">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <v-icon icon="mdi-fire" size="32" color="grey-darken-3" class="mb-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <h1 class="font-bold text-xl">Authoirzed</h1>
 | 
				
			||||||
 | 
					              <p>You're done! We sucessfully established connection between you and {{ metadata?.name }}.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <p class="mt-3">Now you can continue your their app, we will redirect you soon.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <p class="mt-3">Teleporting you to...</p>
 | 
				
			||||||
 | 
					              <p class="text-xs text-mono">{{ route.query["redirect_uri"] }}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-window-item>
 | 
				
			||||||
 | 
					        </v-window>
 | 
				
			||||||
 | 
					      </v-card-text>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <copyright />
 | 
				
			||||||
 | 
					  </v-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { computed, ref } from "vue"
 | 
				
			||||||
 | 
					import { useRoute } from "vue-router"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { getAtk } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { claims, type ClaimType } from "@/views/auth/claims"
 | 
				
			||||||
 | 
					import Copyright from "@/components/Copyright.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const metadata = ref<any>(null)
 | 
				
			||||||
 | 
					const requestedClaims = computed(() => {
 | 
				
			||||||
 | 
					  const scope: string = (route.query["scope"] as string) ?? ""
 | 
				
			||||||
 | 
					  return scope.split(" ")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const panel = ref("confirm")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function tryAuthorize() {
 | 
				
			||||||
 | 
					  const res = await request(`/api/auth/o/authorize${location.search}`, {
 | 
				
			||||||
 | 
					    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (data["ticket"]) {
 | 
				
			||||||
 | 
					      panel.value = "callback"
 | 
				
			||||||
 | 
					      callback(data["ticket"])
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      document.title = `Solarpass | Connect to ${data["client"]?.name}`
 | 
				
			||||||
 | 
					      metadata.value = data["client"]
 | 
				
			||||||
 | 
					      loading.value = false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tryAuthorize()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decline() {
 | 
				
			||||||
 | 
					  if (window.history.length > 0) {
 | 
				
			||||||
 | 
					    window.history.back()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    window.close()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function approve() {
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  const res = await request(
 | 
				
			||||||
 | 
					    "/api/auth/o/authorize?" +
 | 
				
			||||||
 | 
					    new URLSearchParams({
 | 
				
			||||||
 | 
					      client_id: route.query["client_id"] as string,
 | 
				
			||||||
 | 
					      redirect_uri: encodeURIComponent(route.query["redirect_uri"] as string),
 | 
				
			||||||
 | 
					      response_type: "code",
 | 
				
			||||||
 | 
					      scope: route.query["scope"] as string,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					    loading.value = false
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					    panel.value = "callback"
 | 
				
			||||||
 | 
					    setTimeout(() => callback(data["ticket"]), 1850)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function callback(ticket: any) {
 | 
				
			||||||
 | 
					  const url = `${route.query["redirect_uri"]}?code=${ticket["grant_token"]}&state=${route.query["state"]}`
 | 
				
			||||||
 | 
					  window.open(url, "_self")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getClaimDescription(key: string): ClaimType {
 | 
				
			||||||
 | 
					  return Object.prototype.hasOwnProperty.call(claims, key) ? claims[key] : {
 | 
				
			||||||
 | 
					    icon: "mdi-asterisk",
 | 
				
			||||||
 | 
					    name: key,
 | 
				
			||||||
 | 
					    description: "Unknown claim...",
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.card-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .card-grid {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-rounded {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/src/views/auth/claims.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								web/src/views/auth/claims.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					export interface ClaimType {
 | 
				
			||||||
 | 
					  icon: string
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  description: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const claims: { [id: string]: ClaimType } = {
 | 
				
			||||||
 | 
					  openid: {
 | 
				
			||||||
 | 
					    icon: "mdi-identifier",
 | 
				
			||||||
 | 
					    name: "Open Identity",
 | 
				
			||||||
 | 
					    description: "Allow them to read your personal information.",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								web/src/views/auth/sign-in.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										67
									
								
								web/src/views/auth/sign-in.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-container class="h-screen flex flex-col gap-3 items-center justify-center">
 | 
				
			||||||
 | 
					    <callback-notify />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
 | 
				
			||||||
 | 
					      <v-card-text class="card-grid pa-9">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
 | 
				
			||||||
 | 
					          <h1 class="text-2xl">Sign in</h1>
 | 
				
			||||||
 | 
					          <p v-if="ticket">We need to verify that the person trying to access your account is you.</p>
 | 
				
			||||||
 | 
					          <p v-else>Sign in via your Solar ID to access the entire Solar Network.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
 | 
				
			||||||
 | 
					          <v-window-item v-for="(k, idx) in Object.keys(panels)" :key="idx" :value="k">
 | 
				
			||||||
 | 
					            <component :is="panels[k]" @swap="(val: string) => (panel = val)" v-model:loading="loading"
 | 
				
			||||||
 | 
					                       v-model:currentFactor="currentFactor" v-model:ticket="ticket" />
 | 
				
			||||||
 | 
					          </v-window-item>
 | 
				
			||||||
 | 
					        </v-window>
 | 
				
			||||||
 | 
					      </v-card-text>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <copyright />
 | 
				
			||||||
 | 
					  </v-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { type Component, ref } from "vue"
 | 
				
			||||||
 | 
					import Copyright from "@/components/Copyright.vue"
 | 
				
			||||||
 | 
					import CallbackNotify from "@/components/auth/CallbackNotify.vue"
 | 
				
			||||||
 | 
					import FactorPicker from "@/components/auth/FactorPicker.vue"
 | 
				
			||||||
 | 
					import FactorApplicator from "@/components/auth/FactorApplicator.vue"
 | 
				
			||||||
 | 
					import AccountAuthenticate from "@/components/auth/Authenticate.vue"
 | 
				
			||||||
 | 
					import AuthenticateCompleted from "@/components/auth/AuthenticateCompleted.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const currentFactor = ref<any>(null)
 | 
				
			||||||
 | 
					const ticket = ref<any>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const panel = ref("authenticate")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const panels: { [id: string]: Component } = {
 | 
				
			||||||
 | 
					  authenticate: AccountAuthenticate,
 | 
				
			||||||
 | 
					  mfa: FactorPicker,
 | 
				
			||||||
 | 
					  applicator: FactorApplicator,
 | 
				
			||||||
 | 
					  completed: AuthenticateCompleted,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.card-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .card-grid {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-rounded {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										162
									
								
								web/src/views/auth/sign-up.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										162
									
								
								web/src/views/auth/sign-up.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-container class="h-screen flex flex-col gap-3 items-center justify-center">
 | 
				
			||||||
 | 
					    <callback-notify />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
 | 
				
			||||||
 | 
					      <v-card-text class="card-grid pa-9">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
 | 
				
			||||||
 | 
					          <h1 class="text-2xl">Create an account</h1>
 | 
				
			||||||
 | 
					          <p>Create an account on Solar Network. Then enjoy all our services.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="flex items-center">
 | 
				
			||||||
 | 
					          <v-form class="flex-grow-1" @submit.prevent="submit">
 | 
				
			||||||
 | 
					            <v-row dense class="mb-3">
 | 
				
			||||||
 | 
					              <v-col :cols="6">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  hide-details
 | 
				
			||||||
 | 
					                  label="Name"
 | 
				
			||||||
 | 
					                  autocomplete="username"
 | 
				
			||||||
 | 
					                  variant="solo"
 | 
				
			||||||
 | 
					                  density="comfortable"
 | 
				
			||||||
 | 
					                  v-model="data.name"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col :cols="6">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  hide-details
 | 
				
			||||||
 | 
					                  label="Nick"
 | 
				
			||||||
 | 
					                  autocomplete="nickname"
 | 
				
			||||||
 | 
					                  variant="solo"
 | 
				
			||||||
 | 
					                  density="comfortable"
 | 
				
			||||||
 | 
					                  v-model="data.nick"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col :cols="12">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  hide-details
 | 
				
			||||||
 | 
					                  label="Email Address"
 | 
				
			||||||
 | 
					                  type="email"
 | 
				
			||||||
 | 
					                  variant="solo"
 | 
				
			||||||
 | 
					                  density="comfortable"
 | 
				
			||||||
 | 
					                  v-model="data.email"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					              <v-col :cols="12">
 | 
				
			||||||
 | 
					                <v-text-field
 | 
				
			||||||
 | 
					                  hide-details
 | 
				
			||||||
 | 
					                  label="Password"
 | 
				
			||||||
 | 
					                  type="password"
 | 
				
			||||||
 | 
					                  autocomplete="new-password"
 | 
				
			||||||
 | 
					                  variant="solo"
 | 
				
			||||||
 | 
					                  density="comfortable"
 | 
				
			||||||
 | 
					                  v-model="data.password"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </v-col>
 | 
				
			||||||
 | 
					            </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <v-expand-transition>
 | 
				
			||||||
 | 
					              <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					                Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					              </v-alert>
 | 
				
			||||||
 | 
					            </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="flex justify-between">
 | 
				
			||||||
 | 
					              <v-btn type="button" variant="plain" color="grey-darken-3" :to="{ name: 'auth.sign-in' }">
 | 
				
			||||||
 | 
					                Sign in
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <v-btn type="submit" variant="text" color="primary" append-icon="mdi-arrow-right" :disabled="loading">
 | 
				
			||||||
 | 
					                Next
 | 
				
			||||||
 | 
					              </v-btn>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </v-card-text>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-dialog v-model="done" class="max-w-[560px]">
 | 
				
			||||||
 | 
					      <v-card title="Congratulations">
 | 
				
			||||||
 | 
					        <template #text>
 | 
				
			||||||
 | 
					          You successfully created an account on Solar Network. Now sign in to your account and start exploring!
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					        <template #actions>
 | 
				
			||||||
 | 
					          <div class="flex flex-grow-1 justify-end">
 | 
				
			||||||
 | 
					            <v-btn @click="callback">Let's go</v-btn>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </v-card>
 | 
				
			||||||
 | 
					    </v-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <copyright />
 | 
				
			||||||
 | 
					  </v-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { useRoute, useRouter } from "vue-router"
 | 
				
			||||||
 | 
					import Copyright from "@/components/Copyright.vue"
 | 
				
			||||||
 | 
					import CallbackNotify from "@/components/auth/CallbackNotify.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const done = ref(false)
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const data = ref({
 | 
				
			||||||
 | 
					  name: "",
 | 
				
			||||||
 | 
					  nick: "",
 | 
				
			||||||
 | 
					  email: "",
 | 
				
			||||||
 | 
					  password: "",
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  const payload = data.value
 | 
				
			||||||
 | 
					  if (!payload.name || !payload.nick || !payload.email || !payload.password) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  const res = await request("/api/users", {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify(payload),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    done.value = true
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  loading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function callback() {
 | 
				
			||||||
 | 
					  if (route.params["closable"]) {
 | 
				
			||||||
 | 
					    window.close()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    router.push({ name: "auth.sign-in" })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.card-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .card-grid {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-rounded {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										104
									
								
								web/src/views/confirm.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										104
									
								
								web/src/views/confirm.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <v-container class="h-screen flex flex-col gap-3 items-center justify-center">
 | 
				
			||||||
 | 
					    <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
 | 
				
			||||||
 | 
					      <v-card-text class="card-grid pa-9">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <v-avatar color="accent" icon="mdi-check-decagram" size="large" class="card-rounded mb-2" />
 | 
				
			||||||
 | 
					          <h1 class="text-2xl">Confirm registration</h1>
 | 
				
			||||||
 | 
					          <p>Confirm your account to keep your account longer than 48 hours.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
 | 
				
			||||||
 | 
					          <v-window-item value="confirm">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <v-expand-transition>
 | 
				
			||||||
 | 
					                <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
 | 
				
			||||||
 | 
					                  Something went wrong... {{ error }}
 | 
				
			||||||
 | 
					                </v-alert>
 | 
				
			||||||
 | 
					              </v-expand-transition>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <v-progress-circular v-if="!error" indeterminate size="32" color="grey-darken-3" class="mb-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <h1 class="font-bold text-xl">Confirming</h1>
 | 
				
			||||||
 | 
					              <p>We are confirming your account. Please stand by, this won't took a long time...</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-window-item>
 | 
				
			||||||
 | 
					          <v-window-item value="callback">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <v-icon icon="mdi-fire" size="32" color="grey-darken-3" class="mb-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <h1 class="font-bold text-xl">Confirmed</h1>
 | 
				
			||||||
 | 
					              <p>You're done! We successfully confirmed your account.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <p class="mt-3">Now you can continue use Solarpass, we will redirect to dashboard you soon.</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </v-window-item>
 | 
				
			||||||
 | 
					        </v-window>
 | 
				
			||||||
 | 
					      </v-card-text>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <copyright />
 | 
				
			||||||
 | 
					  </v-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue"
 | 
				
			||||||
 | 
					import { useRoute, useRouter } from "vue-router"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import Copyright from "@/components/Copyright.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const { readProfiles } = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const panel = ref("confirm")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function confirm() {
 | 
				
			||||||
 | 
					  if (!route.query["tk"]) {
 | 
				
			||||||
 | 
					    error.value = "code was not exists"
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const res = await request("/api/users/me/confirm", {
 | 
				
			||||||
 | 
					    method: "POST",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
					    body: JSON.stringify({
 | 
				
			||||||
 | 
					      code: route.query["tk"],
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    loading.value = true
 | 
				
			||||||
 | 
					    panel.value = "callback"
 | 
				
			||||||
 | 
					    await readProfiles()
 | 
				
			||||||
 | 
					    router.push({ name: "dashboard" })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  loading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					confirm()
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.card-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .card-grid {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-rounded {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										77
									
								
								web/src/views/dashboard.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										77
									
								
								web/src/views/dashboard.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-card>
 | 
				
			||||||
 | 
					      <v-img cover class="bg-grey-lighten-2" :height="240" :src="'/api/avatar/' + id.userinfo.data.banner" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-card-text class="flex gap-3.5 px-5 pb-5">
 | 
				
			||||||
 | 
					        <v-avatar
 | 
				
			||||||
 | 
					          color="grey-lighten-2"
 | 
				
			||||||
 | 
					          icon="mdi-account-circle"
 | 
				
			||||||
 | 
					          class="rounded-card"
 | 
				
			||||||
 | 
					          :size="54"
 | 
				
			||||||
 | 
					          :image="'/api/avatar/' + id.userinfo.data.avatar"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h1 class="text-2xl cursor-pointer" @click="show.realname = !show.realname">{{ displayName }}</h1>
 | 
				
			||||||
 | 
					          <p v-html="description"></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div class="mt-5">
 | 
				
			||||||
 | 
					            <p class="opacity-80 desc-line">
 | 
				
			||||||
 | 
					              <v-icon icon="mdi-calendar-blank" size="16" />
 | 
				
			||||||
 | 
					              <span>Joined at {{ new Date(id.userinfo.data?.created_at)?.toLocaleString() }}</span>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            <p class="opacity-80 desc-line">
 | 
				
			||||||
 | 
					              <v-icon icon="mdi-cake-variant" size="16" />
 | 
				
			||||||
 | 
					              <span>Birthday is {{ new Date(id.userinfo.data?.profile.birthday)?.toLocaleString() }}</span>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </v-card-text>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useUserinfo } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { computed } from "vue"
 | 
				
			||||||
 | 
					import { reactive } from "vue"
 | 
				
			||||||
 | 
					import { parse } from "marked"
 | 
				
			||||||
 | 
					import dompurify from "dompurify"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const id = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const displayName = computed(() => {
 | 
				
			||||||
 | 
					  if (show.realname) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      (id.userinfo.data?.profile?.first_name ?? "Unknown") + " " + (id.userinfo.data?.profile?.last_name ?? "Unknown")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return id.userinfo.displayName
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					const description = computed(() => {
 | 
				
			||||||
 | 
					  if (id.userinfo.data?.description) {
 | 
				
			||||||
 | 
					    return dompurify().sanitize(parse(id.userinfo.data?.description) as string)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return "No description yet."
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const show = reactive({
 | 
				
			||||||
 | 
					  realname: false,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.desc-line {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.rounded-card {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										157
									
								
								web/src/views/personalize.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										157
									
								
								web/src/views/personalize.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <GoUseSolian class="mb-3" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-card title="Information" prepend-icon="mdi-face-man-profile" :loading="loading">
 | 
				
			||||||
 | 
					      <template #text>
 | 
				
			||||||
 | 
					        <v-form class="mt-1" @submit.prevent="submit">
 | 
				
			||||||
 | 
					          <v-row dense>
 | 
				
			||||||
 | 
					            <v-col :xs="12" :md="6">
 | 
				
			||||||
 | 
					              <v-text-field readonly hide-details label="Username" density="comfortable" variant="outlined"
 | 
				
			||||||
 | 
					                v-model="data.name" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col :xs="12" :md="6">
 | 
				
			||||||
 | 
					              <v-text-field hide-details label="Nickname" density="comfortable" variant="outlined"
 | 
				
			||||||
 | 
					                v-model="data.nick" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col :cols="12">
 | 
				
			||||||
 | 
					              <v-textarea hide-details label="Description" density="comfortable" variant="outlined"
 | 
				
			||||||
 | 
					                v-model="data.description" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col :xs="12" :md="6" :lg="4">
 | 
				
			||||||
 | 
					              <v-text-field hide-details label="First Name" density="comfortable" variant="outlined"
 | 
				
			||||||
 | 
					                v-model="data.first_name" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col :xs="12" :md="6" :lg="4">
 | 
				
			||||||
 | 
					              <v-text-field hide-details label="Last Name" density="comfortable" variant="outlined"
 | 
				
			||||||
 | 
					                v-model="data.last_name" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					            <v-col :xs="12" :lg="4">
 | 
				
			||||||
 | 
					              <v-text-field hide-details label="Birthday" density="comfortable" variant="outlined" type="datetime-local"
 | 
				
			||||||
 | 
					                v-model="data.birthday" />
 | 
				
			||||||
 | 
					            </v-col>
 | 
				
			||||||
 | 
					          </v-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <v-btn type="submit" class="mt-2" variant="text" prepend-icon="mdi-content-save" :disabled="loading">
 | 
				
			||||||
 | 
					            Apply Changes
 | 
				
			||||||
 | 
					          </v-btn>
 | 
				
			||||||
 | 
					        </v-form>
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					    </v-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <v-snackbar v-model="done" :timeout="3000"> Your personal information has been updated. </v-snackbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- @vue-ignore -->
 | 
				
			||||||
 | 
					    <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, watch } from "vue"
 | 
				
			||||||
 | 
					import { useUserinfo, getAtk } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import GoUseSolian from "@/components/GoUseSolian.vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const id = useUserinfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					const done = ref(false)
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const data = ref<any>({})
 | 
				
			||||||
 | 
					const avatar = ref<any>(null)
 | 
				
			||||||
 | 
					const banner = ref<any>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  id,
 | 
				
			||||||
 | 
					  (val) => {
 | 
				
			||||||
 | 
					    if (val.isReady) {
 | 
				
			||||||
 | 
					      data.value.name = id.userinfo.data.name
 | 
				
			||||||
 | 
					      data.value.nick = id.userinfo.data.nick
 | 
				
			||||||
 | 
					      data.value.description = id.userinfo.data.description
 | 
				
			||||||
 | 
					      data.value.first_name = id.userinfo.data.profile.first_name
 | 
				
			||||||
 | 
					      data.value.last_name = id.userinfo.data.profile.last_name
 | 
				
			||||||
 | 
					      data.value.birthday = id.userinfo.data.profile.birthday
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (data.value.birthday) data.value.birthday = data.value.birthday.substring(0, 16)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { immediate: true, deep: true },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  const payload = data.value
 | 
				
			||||||
 | 
					  if (payload.birthday) payload.birthday = new Date(payload.birthday).toISOString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  const res = await request("/api/users/me", {
 | 
				
			||||||
 | 
					    method: "PUT",
 | 
				
			||||||
 | 
					    headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    body: JSON.stringify(payload),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    await id.readProfiles()
 | 
				
			||||||
 | 
					    done.value = true
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  loading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function applyAvatar() {
 | 
				
			||||||
 | 
					  if (!avatar.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (loading.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const payload = new FormData()
 | 
				
			||||||
 | 
					  payload.set("avatar", avatar.value[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  const res = await request("/api/users/me/avatar", {
 | 
				
			||||||
 | 
					    method: "PUT",
 | 
				
			||||||
 | 
					    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    body: payload,
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    await id.readProfiles()
 | 
				
			||||||
 | 
					    done.value = true
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					    avatar.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  loading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function applyBanner() {
 | 
				
			||||||
 | 
					  if (!banner.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (loading.value) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const payload = new FormData()
 | 
				
			||||||
 | 
					  payload.set("banner", banner.value[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  const res = await request("/api/users/me/banner", {
 | 
				
			||||||
 | 
					    method: "PUT",
 | 
				
			||||||
 | 
					    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    body: payload,
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    await id.readProfiles()
 | 
				
			||||||
 | 
					    done.value = true
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					    banner.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  loading.value = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.rounded-card {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										197
									
								
								web/src/views/security.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										197
									
								
								web/src/views/security.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,197 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <v-expansion-panels>
 | 
				
			||||||
 | 
					      <v-expansion-panel eager title="Tickets">
 | 
				
			||||||
 | 
					        <template #text>
 | 
				
			||||||
 | 
					          <v-card :loading="reverting.tickets" variant="outlined">
 | 
				
			||||||
 | 
					            <v-data-table-server
 | 
				
			||||||
 | 
					              density="compact"
 | 
				
			||||||
 | 
					              :headers="dataDefinitions.tickets"
 | 
				
			||||||
 | 
					              :items="tickets"
 | 
				
			||||||
 | 
					              :items-length="pagination.tickets.total"
 | 
				
			||||||
 | 
					              :loading="reverting.tickets"
 | 
				
			||||||
 | 
					              v-model:items-per-page="pagination.tickets.pageSize"
 | 
				
			||||||
 | 
					              @update:options="readTickets"
 | 
				
			||||||
 | 
					              item-value="id"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <template v-slot:item="{ item }: { item: any }">
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                  <td>{{ item.id }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.ip_address }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-tooltip :text="item.user_agent" location="top">
 | 
				
			||||||
 | 
					                      <template #activator="{ props }">
 | 
				
			||||||
 | 
					                        <div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[280px]">
 | 
				
			||||||
 | 
					                          {{ item.user_agent }}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </template>
 | 
				
			||||||
 | 
					                    </v-tooltip>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-tooltip text="Sign out">
 | 
				
			||||||
 | 
					                      <template #activator="{ props }">
 | 
				
			||||||
 | 
					                        <v-btn
 | 
				
			||||||
 | 
					                          v-bind="props"
 | 
				
			||||||
 | 
					                          variant="text"
 | 
				
			||||||
 | 
					                          size="x-small"
 | 
				
			||||||
 | 
					                          color="error"
 | 
				
			||||||
 | 
					                          icon="mdi-logout-variant"
 | 
				
			||||||
 | 
					                          @click="killTicket(item)"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                      </template>
 | 
				
			||||||
 | 
					                    </v-tooltip>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					            </v-data-table-server>
 | 
				
			||||||
 | 
					          </v-card>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </v-expansion-panel>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <v-expansion-panel eager title="Events">
 | 
				
			||||||
 | 
					        <template #text>
 | 
				
			||||||
 | 
					          <v-card :loading="reverting.events" variant="outlined">
 | 
				
			||||||
 | 
					            <v-data-table-server
 | 
				
			||||||
 | 
					              density="compact"
 | 
				
			||||||
 | 
					              :headers="dataDefinitions.events"
 | 
				
			||||||
 | 
					              :items="events"
 | 
				
			||||||
 | 
					              :items-length="pagination.events.total"
 | 
				
			||||||
 | 
					              :loading="reverting.events"
 | 
				
			||||||
 | 
					              v-model:items-per-page="pagination.events.pageSize"
 | 
				
			||||||
 | 
					              @update:options="readEvents"
 | 
				
			||||||
 | 
					              item-value="id"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <template v-slot:item="{ item }: { item: any }">
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                  <td>{{ item.id }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.type }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.target }}</td>
 | 
				
			||||||
 | 
					                  <td>{{ item.ip_address }}</td>
 | 
				
			||||||
 | 
					                  <td>
 | 
				
			||||||
 | 
					                    <v-tooltip :text="item.user_agent" location="top">
 | 
				
			||||||
 | 
					                      <template #activator="{ props }">
 | 
				
			||||||
 | 
					                        <div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[180px]">
 | 
				
			||||||
 | 
					                          {{ item.user_agent }}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </template>
 | 
				
			||||||
 | 
					                    </v-tooltip>
 | 
				
			||||||
 | 
					                  </td>
 | 
				
			||||||
 | 
					                  <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					            </v-data-table-server>
 | 
				
			||||||
 | 
					          </v-card>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </v-expansion-panel>
 | 
				
			||||||
 | 
					    </v-expansion-panels>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { request } from "@/scripts/request"
 | 
				
			||||||
 | 
					import { getAtk } from "@/stores/userinfo"
 | 
				
			||||||
 | 
					import { reactive, ref } from "vue"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = ref<string | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dataDefinitions: { [id: string]: any[] } = {
 | 
				
			||||||
 | 
					  tickets: [
 | 
				
			||||||
 | 
					    { align: "start", key: "id", title: "ID" },
 | 
				
			||||||
 | 
					    { align: "start", key: "ip_address", title: "IP Address" },
 | 
				
			||||||
 | 
					    { align: "start", key: "user_agent", title: "User Agent" },
 | 
				
			||||||
 | 
					    { align: "start", key: "created_at", title: "Issued At" },
 | 
				
			||||||
 | 
					    { align: "start", key: "actions", title: "Actions", sortable: false },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  events: [
 | 
				
			||||||
 | 
					    { align: "start", key: "id", title: "ID" },
 | 
				
			||||||
 | 
					    { align: "start", key: "type", title: "Type" },
 | 
				
			||||||
 | 
					    { align: "start", key: "target", title: "Affected Object" },
 | 
				
			||||||
 | 
					    { align: "start", key: "ip_address", title: "IP Address" },
 | 
				
			||||||
 | 
					    { align: "start", key: "user_agent", title: "User Agent" },
 | 
				
			||||||
 | 
					    { align: "start", key: "created_at", title: "Performed At" },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tickets = ref<any>([])
 | 
				
			||||||
 | 
					const events = ref<any>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reverting = reactive({ tickets: false, sessions: false, events: false })
 | 
				
			||||||
 | 
					const pagination = reactive({
 | 
				
			||||||
 | 
					  tickets: { page: 1, pageSize: 5, total: 0 },
 | 
				
			||||||
 | 
					  events: { page: 1, pageSize: 5, total: 0 },
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
				
			||||||
 | 
					  if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage
 | 
				
			||||||
 | 
					  if (page) pagination.tickets.page = page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reverting.sessions = true
 | 
				
			||||||
 | 
					  const res = await request(
 | 
				
			||||||
 | 
					    "/api/users/me/tickets?" +
 | 
				
			||||||
 | 
					      new URLSearchParams({
 | 
				
			||||||
 | 
					        take: pagination.tickets.pageSize.toString(),
 | 
				
			||||||
 | 
					        offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					    tickets.value = data["data"]
 | 
				
			||||||
 | 
					    pagination.tickets.total = data["count"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  reverting.sessions = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
				
			||||||
 | 
					  if (itemsPerPage) pagination.events.pageSize = itemsPerPage
 | 
				
			||||||
 | 
					  if (page) pagination.events.page = page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reverting.events = true
 | 
				
			||||||
 | 
					  const res = await request(
 | 
				
			||||||
 | 
					    "/api/users/me/events?" +
 | 
				
			||||||
 | 
					      new URLSearchParams({
 | 
				
			||||||
 | 
					        take: pagination.events.pageSize.toString(),
 | 
				
			||||||
 | 
					        offset: ((pagination.events.page - 1) * pagination.events.pageSize).toString(),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const data = await res.json()
 | 
				
			||||||
 | 
					    events.value = data["data"]
 | 
				
			||||||
 | 
					    pagination.events.total = data["count"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  reverting.events = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Promise.all([readTickets({}), readEvents({})])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function killTicket(item: any) {
 | 
				
			||||||
 | 
					  reverting.sessions = true
 | 
				
			||||||
 | 
					  const res = await request(`/api/users/me/tickets/${item.id}`, {
 | 
				
			||||||
 | 
					    method: "DELETE",
 | 
				
			||||||
 | 
					    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (res.status !== 200) {
 | 
				
			||||||
 | 
					    error.value = await res.text()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    await readTickets({})
 | 
				
			||||||
 | 
					    error.value = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  reverting.sessions = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.rounded-card {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										14
									
								
								web/tsconfig.app.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								web/tsconfig.app.json
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "@vue/tsconfig/tsconfig.dom.json",
 | 
				
			||||||
 | 
					  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
 | 
				
			||||||
 | 
					  "exclude": ["src/**/__tests__/*"],
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "composite": true,
 | 
				
			||||||
 | 
					    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "baseUrl": ".",
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@/*": ["./src/*"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								web/tsconfig.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								web/tsconfig.json
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "files": [],
 | 
				
			||||||
 | 
					  "references": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "path": "./tsconfig.node.json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "path": "./tsconfig.app.json"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/tsconfig.node.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								web/tsconfig.node.json
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "@tsconfig/node20/tsconfig.json",
 | 
				
			||||||
 | 
					  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"],
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "composite": true,
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "moduleResolution": "Bundler",
 | 
				
			||||||
 | 
					    "types": ["node"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								web/uno.config.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								web/uno.config.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					import { defineConfig, presetAttributify, presetTypography, presetUno } from "unocss"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  presets: [presetAttributify(), presetTypography(), presetUno({ preflight: false })],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										27
									
								
								web/vite.config.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										27
									
								
								web/vite.config.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { fileURLToPath, URL } from "node:url";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
 | 
					import vue from "@vitejs/plugin-vue";
 | 
				
			||||||
 | 
					import vueJsx from "@vitejs/plugin-vue-jsx";
 | 
				
			||||||
 | 
					import unocss from "unocss/vite";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  plugins: [vue(), vueJsx(), unocss()],
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      "@": fileURLToPath(new URL("./src", import.meta.url))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    proxy: {
 | 
				
			||||||
 | 
					      "/api/ws": {
 | 
				
			||||||
 | 
					        target: "ws://localhost:8444",
 | 
				
			||||||
 | 
					        ws: true
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      "/api": "http://localhost:8444",
 | 
				
			||||||
 | 
					      "/.well-known": "http://localhost:8444"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Reference in New Issue
	
	Block a user