Password reset & user lookup API

This commit is contained in:
LittleSheep 2024-06-30 17:20:05 +08:00
parent e5d8f1ab3b
commit a4ccf12b7a
6 changed files with 71 additions and 41 deletions

View File

@ -4,18 +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=":recycle: Improve notify API"> <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Reset password APIs">
<change afterPath="$PROJECT_DIR$/web/src/views/flow/password-reset.vue" afterDir="false" />
<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/internal/models/tokens.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/models/tokens.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" afterDir="false" /> <change beforePath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/factors_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/factors_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/index.go" afterDir="false" /> <change beforePath="$PROJECT_DIR$/pkg/internal/server/api/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/index.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/security_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/security_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/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$/web/src/views/flow/confirm.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/flow/confirm.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/src/router/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/router/index.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/web/src/views/flow/password-reset.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/flow/password-reset.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/src/views/confirm.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/flow/confirm.vue" 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" />
@ -161,7 +156,6 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value=":card_file_box: Add the status model" />
<MESSAGE value=":bug: Authenticate wrong payload hotfix" /> <MESSAGE value=":bug: Authenticate wrong payload hotfix" />
<MESSAGE value=":sparkles: Can pick up mfa request" /> <MESSAGE value=":sparkles: Can pick up mfa request" />
<MESSAGE value=":sparkles: Status system" /> <MESSAGE value=":sparkles: Status system" />
@ -186,7 +180,8 @@
<MESSAGE value=":bug: Fix request body validation" /> <MESSAGE value=":bug: Fix request body validation" />
<MESSAGE value=":bug: Fix API mapping issue" /> <MESSAGE value=":bug: Fix API mapping issue" />
<MESSAGE value=":recycle: Improve notify API" /> <MESSAGE value=":recycle: Improve notify API" />
<option name="LAST_COMMIT_MESSAGE" value=":recycle: Improve notify API" /> <MESSAGE value=":sparkles: Reset password APIs" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Reset password APIs" />
</component> </component>
<component name="VgoProject"> <component name="VgoProject">
<settings-migrated>true</settings-migrated> <settings-migrated>true</settings-migrated>

View File

@ -14,6 +14,20 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
func lookupAccount(c *fiber.Ctx) error {
probe := c.Query("probe")
if len(probe) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "you must provide a probe")
}
user, err := services.LookupAccount(probe)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(user)
}
func getUserinfo(c *fiber.Ctx) error { func getUserinfo(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil { if err := exts.EnsureAuthenticated(c); err != nil {
return err return err

View File

@ -19,6 +19,8 @@ func MapAPIs(app *fiber.App) {
notify.Put("/:notificationId/read", markNotificationRead) notify.Put("/:notificationId/read", markNotificationRead)
} }
api.Get("/users/lookup", lookupAccount)
me := api.Group("/users/me").Name("Myself Operations") me := api.Group("/users/me").Name("Myself Operations")
{ {
@ -34,8 +36,8 @@ func MapAPIs(app *fiber.App) {
me.Delete("/tickets/:ticketId", killTicket) me.Delete("/tickets/:ticketId", killTicket)
me.Post("/confirm", doRegisterConfirm) me.Post("/confirm", doRegisterConfirm)
me.Post("/reset-password", requestResetPassword) me.Post("/password-reset", requestResetPassword)
me.Patch("/reset-password", confirmResetPassword) me.Patch("/password-reset", confirmResetPassword)
me.Get("/status", getMyselfStatus) me.Get("/status", getMyselfStatus)
me.Post("/status", setStatus) me.Post("/status", setStatus)

View File

@ -141,10 +141,11 @@ func CheckAbleToResetPassword(user models.Account) error {
if err := database.C. if err := database.C.
Where("account_id = ?", user.ID). Where("account_id = ?", user.ID).
Where("expired_at < ?", time.Now()). Where("expired_at < ?", time.Now()).
Where("type = ?", models.ResetPasswordMagicToken).
Model(&models.MagicToken{}). Model(&models.MagicToken{}).
Count(&count).Error; err != nil { Count(&count).Error; err != nil {
return fmt.Errorf("unable to check reset password ability: %v", err) return fmt.Errorf("unable to check reset password ability: %v", err)
} else if count == 0 { } else if count > 0 {
return fmt.Errorf("you requested reset password recently") return fmt.Errorf("you requested reset password recently")
} }

View File

@ -5,7 +5,7 @@
<div> <div>
<v-avatar color="accent" icon="mdi-check-decagram" size="large" class="card-rounded mb-2" /> <v-avatar color="accent" icon="mdi-check-decagram" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">Confirm registration</h1> <h1 class="text-2xl">Confirm registration</h1>
<p>Confirm your account to keep your account longer than 48 hours.</p> <p>Confirm your account to unlock more abilities.</p>
</div> </div>
<v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]"> <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
@ -30,7 +30,7 @@
<h1 class="font-bold text-xl">Confirmed</h1> <h1 class="font-bold text-xl">Confirmed</h1>
<p>You're done! We successfully confirmed your account.</p> <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> <p class="mt-3">Now you can continue to use Solarpass, we will redirect you to dashboard soon.</p>
</div> </div>
</v-window-item> </v-window-item>
</v-window> </v-window>
@ -77,7 +77,7 @@ async function confirm() {
loading.value = true loading.value = true
panel.value = "callback" panel.value = "callback"
await readProfiles() await readProfiles()
router.push({ name: "dashboard" }) await router.push({ name: "dashboard" })
} }
loading.value = false loading.value = false
} }

View File

@ -3,34 +3,54 @@
<v-card class="w-full max-w-[720px] overflow-auto" :loading="loading"> <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading">
<v-card-text class="card-grid pa-9"> <v-card-text class="card-grid pa-9">
<div> <div>
<v-avatar color="accent" icon="mdi-check-decagram" size="large" class="card-rounded mb-2" /> <v-avatar color="accent" icon="mdi-lock-reset" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">Confirm registration</h1> <h1 class="text-2xl">Reset password</h1>
<p>Confirm your account to keep your account longer than 48 hours.</p> <p>Reset password to get back access of your account.</p>
</div> </div>
<v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]"> <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
<v-window-item value="confirm"> <v-window-item value="confirm">
<div> <div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="confirm">
<v-text-field
label="New Password"
type="password"
autocomplete="new-password"
variant="solo"
density="comfortable"
:disabled="loading"
v-model="newPassword"
/>
<v-expand-transition> <v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3"> <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }} Something went wrong... {{ error }}
</v-alert> </v-alert>
</v-expand-transition> </v-expand-transition>
<v-progress-circular v-if="!error" indeterminate size="32" color="grey-darken-3" class="mb-3" /> <div class="flex justify-end">
<v-btn
<h1 class="font-bold text-xl">Confirming</h1> type="submit"
<p>We are confirming your account. Please stand by, this won't took a long time...</p> variant="text"
color="primary"
class="justify-self-end"
append-icon="mdi-arrow-right"
:disabled="loading"
>
Next
</v-btn>
</div>
</v-form>
</div> </div>
</v-window-item> </v-window-item>
<v-window-item value="callback"> <v-window-item value="callback">
<div> <div>
<v-icon icon="mdi-fire" size="32" color="grey-darken-3" class="mb-3" /> <v-icon icon="mdi-fire" size="32" color="grey-darken-3" class="mb-3" />
<h1 class="font-bold text-xl">Confirmed</h1> <h1 class="font-bold text-xl">Applied</h1>
<p>You're done! We successfully confirmed your account.</p> <p>The password of your account has updated successfully.</p>
<p class="mt-3">Now you can continue use Solarpass, we will redirect to dashboard you soon.</p> <p class="mt-3">Now you can continue to use Solarpass, we will redirect you to sign-in soon.</p>
</div> </div>
</v-window-item> </v-window-item>
</v-window> </v-window>
@ -45,12 +65,10 @@
import { ref } from "vue" import { ref } from "vue"
import { useRoute, useRouter } from "vue-router" import { useRoute, useRouter } from "vue-router"
import { request } from "@/scripts/request" import { request } from "@/scripts/request"
import { useUserinfo } from "@/stores/userinfo"
import Copyright from "@/components/Copyright.vue" import Copyright from "@/components/Copyright.vue"
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { readProfiles } = useUserinfo()
const error = ref<string | null>(null) const error = ref<string | null>(null)
@ -58,17 +76,20 @@ const loading = ref(false)
const panel = ref("confirm") const panel = ref("confirm")
const newPassword = ref("")
async function confirm() { async function confirm() {
if (!route.query["tk"]) { if (!route.query["code"]) {
error.value = "code was not exists" error.value = "code was not exists"
return return
} }
const res = await request("/api/users/me/confirm", { const res = await request("/api/users/me/password-reset", {
method: "POST", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
code: route.query["tk"], code: route.query["code"],
new_password: newPassword.value,
}), }),
}) })
if (res.status !== 200) { if (res.status !== 200) {
@ -76,13 +97,10 @@ async function confirm() {
} else { } else {
loading.value = true loading.value = true
panel.value = "callback" panel.value = "callback"
await readProfiles() await router.push({ name: "auth.sign-in" })
router.push({ name: "dashboard" })
} }
loading.value = false loading.value = false
} }
confirm()
</script> </script>
<style scoped> <style scoped>