diff --git a/api/.env.example b/api/.env.example index 445431f0..9169930d 100644 --- a/api/.env.example +++ b/api/.env.example @@ -55,7 +55,8 @@ PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -JWT_TTL=1440 +JWT_TTL=10080 +JWT_REMEMBER_TTL=43200 JWT_SECRET= STRIPE_KEY= diff --git a/api/app/Http/Controllers/Admin/ImpersonationController.php b/api/app/Http/Controllers/Admin/ImpersonationController.php index ac9c0dea..8c6ea062 100644 --- a/api/app/Http/Controllers/Admin/ImpersonationController.php +++ b/api/app/Http/Controllers/Admin/ImpersonationController.php @@ -41,6 +41,8 @@ class ImpersonationController extends Controller return $this->success([ 'token' => $token, + 'token_type' => 'bearer', + 'expires_in' => auth()->getPayload()->get('exp') - time(), ]); } } diff --git a/api/app/Http/Controllers/Auth/LoginController.php b/api/app/Http/Controllers/Auth/LoginController.php index 2b2913aa..ff36c388 100644 --- a/api/app/Http/Controllers/Auth/LoginController.php +++ b/api/app/Http/Controllers/Auth/LoginController.php @@ -30,7 +30,15 @@ class LoginController extends Controller */ protected function attemptLogin(Request $request) { - $token = $this->guard()->attempt($this->credentials($request)); + // Only set custom TTL if remember me is checked + $guard = $this->guard(); + + if ($request->remember) { + // Use the extended TTL from config for "Remember me" + $guard->setTTL(config('jwt.remember_ttl')); + } + + $token = $guard->attempt($this->credentials($request)); if (! $token) { return false; diff --git a/api/config/jwt.php b/api/config/jwt.php index 09a3342e..5a81341f 100644 --- a/api/config/jwt.php +++ b/api/config/jwt.php @@ -101,7 +101,19 @@ return [ | */ - 'ttl' => (int) env('JWT_TTL', 60), + 'ttl' => (int) env('JWT_TTL', 60 * 24 * 7), + + /* + |-------------------------------------------------------------------------- + | Extended JWT time to live (Remember Me) + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for + | when the "Remember me" option is selected. Defaults to 30 days. + | + */ + + 'remember_ttl' => (int) env('JWT_REMEMBER_TTL', 60 * 24 * 30), /* |-------------------------------------------------------------------------- diff --git a/client/components/pages/admin/ImpersonateUser.vue b/client/components/pages/admin/ImpersonateUser.vue index c85ed9bb..73e017d4 100644 --- a/client/components/pages/admin/ImpersonateUser.vue +++ b/client/components/pages/admin/ImpersonateUser.vue @@ -27,8 +27,8 @@ const impersonate = () => { opnFetch(`/moderator/impersonate/${props.user.id}`).then(async (data) => { loading.value = false - // Save the token. - authStore.setToken(data.token, false) + // Save the token with its expiration time. + authStore.setToken(data.token, data.expires_in) // Fetch the user. const userData = await opnFetch('user') diff --git a/client/components/pages/auth/components/LoginForm.vue b/client/components/pages/auth/components/LoginForm.vue index 2b69d6e7..0014ddb9 100644 --- a/client/components/pages/auth/components/LoginForm.vue +++ b/client/components/pages/auth/components/LoginForm.vue @@ -141,10 +141,10 @@ export default { // Submit the form. this.loading = true this.form - .post("login") + .post("login", { data: { remember: this.remember } }) .then(async (data) => { - // Save the token. - this.authStore.setToken(data.token) + // Save the token with its expiration time + this.authStore.setToken(data.token, data.expires_in) const [userDataResponse, workspacesResponse] = await Promise.all([ opnFetch("user"), diff --git a/client/components/pages/auth/components/RegisterForm.vue b/client/components/pages/auth/components/RegisterForm.vue index c77290bb..96a33b55 100644 --- a/client/components/pages/auth/components/RegisterForm.vue +++ b/client/components/pages/auth/components/RegisterForm.vue @@ -244,10 +244,10 @@ export default { } // Log in the user. - const tokenData = await this.form.post("/login") + const tokenData = await this.form.post("/login", { data: { remember: true } }) - // Save the token. - this.authStore.setToken(tokenData.token) + // Save the token with its expiration time. + this.authStore.setToken(tokenData.token, tokenData.expires_in) const userData = await opnFetch("user") this.authStore.setUser(userData) diff --git a/client/middleware/01.check-auth.global.js b/client/middleware/01.check-auth.global.js index 9db0b20b..2959bef8 100644 --- a/client/middleware/01.check-auth.global.js +++ b/client/middleware/01.check-auth.global.js @@ -2,7 +2,13 @@ import { fetchAllWorkspaces } from "~/stores/workspaces.js" export default defineNuxtRouteMiddleware(async () => { const authStore = useAuthStore() - authStore.initStore(useCookie("token").value, useCookie("admin_token").value) + + // Get tokens from cookies + const tokenValue = useCookie("token").value + const adminTokenValue = useCookie("admin_token").value + + // Initialize the store with the tokens + authStore.initStore(tokenValue, adminTokenValue) if (authStore.token && !authStore.user) { const workspaceStore = useWorkspacesStore() diff --git a/client/pages/oauth/callback.vue b/client/pages/oauth/callback.vue index 71940130..110a46bf 100644 --- a/client/pages/oauth/callback.vue +++ b/client/pages/oauth/callback.vue @@ -54,7 +54,7 @@ function handleCallback() { form.code = route.query.code form.utm_data = $utm.value form.post(`/oauth/${provider}/callback`).then(async (data) => { - authStore.setToken(data.token) + authStore.setToken(data.token, data.expires_in) const [userDataResponse, workspacesResponse] = await Promise.all([ opnFetch("user"), fetchAllWorkspaces(), diff --git a/client/stores/auth.js b/client/stores/auth.js index 818c4c01..8a987164 100644 --- a/client/stores/auth.js +++ b/client/stores/auth.js @@ -21,12 +21,22 @@ export const useAuthStore = defineStore("auth", { }, // Stop admin impersonation stopImpersonating() { - this.setToken(this.admin_token) + // When stopping impersonation, we don't have expiration info for the admin token + // Use a default long expiration (24 hours) to ensure the admin can continue working + this.setToken(this.admin_token, 60 * 60 * 24) this.setAdminToken(null) }, - setToken(token) { - this.setCookie("token", token) + setToken(token, expiresIn) { + // Set cookie with expiration if provided + const cookieOptions = {} + + if (expiresIn) { + // expiresIn is in seconds, maxAge also needs to be in seconds + cookieOptions.maxAge = expiresIn + } + + this.setCookie("token", token, cookieOptions) this.token = token }, @@ -35,9 +45,9 @@ export const useAuthStore = defineStore("auth", { this.admin_token = token }, - setCookie(name, value) { + setCookie(name, value, options = {}) { if (import.meta.client) { - useCookie(name).value = value + useCookie(name, options).value = value } }, @@ -49,7 +59,8 @@ export const useAuthStore = defineStore("auth", { setUser(user) { if (!user) { console.error("No user, logging out.") - this.setToken(null) + // When logging out due to no user, clear the token with maxAge 0 + this.setToken(null, 0) } this.user = user @@ -73,7 +84,10 @@ export const useAuthStore = defineStore("auth", { opnFetch("logout", { method: "POST" }).catch(() => {}) this.user = null - this.setToken(null) + + // Clear the token cookie by setting maxAge to 0 + this.setCookie("token", null, { maxAge: 0 }) + this.token = null }, // async fetchOauthUrl() {