Merge pull request #35 from StewKI/feature-client-with-working-auth
Feature client with working auth
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
using AipsCore.Application.Abstract.Query;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.User.Query.GetMe;
|
||||||
|
|
||||||
|
public record GetMeQuery : IQuery<GetMeQueryDto>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace AipsCore.Application.Models.User.Query.GetMe;
|
||||||
|
|
||||||
|
public record GetMeQueryDto(string UserName);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using AipsCore.Application.Abstract.Query;
|
||||||
|
using AipsCore.Application.Abstract.UserContext;
|
||||||
|
using AipsCore.Domain.Common.Validation;
|
||||||
|
using AipsCore.Domain.Models.User.Validation;
|
||||||
|
using AipsCore.Domain.Models.User.ValueObjects;
|
||||||
|
using AipsCore.Infrastructure.Persistence.Db;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.User.Query.GetMe;
|
||||||
|
|
||||||
|
public class GetMeQueryHandler : IQueryHandler<GetMeQuery, GetMeQueryDto>
|
||||||
|
{
|
||||||
|
private readonly AipsDbContext _context;
|
||||||
|
private readonly IUserContext _userContext;
|
||||||
|
|
||||||
|
public GetMeQueryHandler(AipsDbContext context, IUserContext userContext)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_userContext = userContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetMeQueryDto> Handle(GetMeQuery query, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var userId = _userContext.GetCurrentUserId();
|
||||||
|
|
||||||
|
var result = await _context.Users
|
||||||
|
.Where(u => u.Id.ToString() == userId.IdValue)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
throw new ValidationException(UserErrors.NotFound(new UserId(userId.IdValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetMeQueryDto(result.UserName!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
using AipsCore.Application.Abstract;
|
using AipsCore.Application.Abstract;
|
||||||
using AipsCore.Application.Common.Authentication.Dtos;
|
using AipsCore.Application.Common.Authentication.Dtos;
|
||||||
using AipsCore.Application.Abstract.MessageBroking;
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
using AipsCore.Application.Common.Authentication;
|
|
||||||
using AipsCore.Application.Common.Message.TestMessage;
|
using AipsCore.Application.Common.Message.TestMessage;
|
||||||
using AipsCore.Application.Models.User.Command.LogIn;
|
using AipsCore.Application.Models.User.Command.LogIn;
|
||||||
using AipsCore.Application.Models.User.Command.LogOut;
|
using AipsCore.Application.Models.User.Command.LogOut;
|
||||||
using AipsCore.Application.Models.User.Command.LogOutAll;
|
using AipsCore.Application.Models.User.Command.LogOutAll;
|
||||||
using AipsCore.Application.Models.User.Command.RefreshLogIn;
|
using AipsCore.Application.Models.User.Command.RefreshLogIn;
|
||||||
using AipsCore.Application.Models.User.Command.SignUp;
|
using AipsCore.Application.Models.User.Command.SignUp;
|
||||||
using AipsCore.Application.Models.User.Query.GetUser;
|
using AipsCore.Application.Models.User.Query.GetMe;
|
||||||
|
using AipsCore.Infrastructure.Persistence.User;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace AipsWebApi.Controllers;
|
namespace AipsWebApi.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("/api/[controller]")]
|
||||||
public class UserController : ControllerBase
|
public class UserController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IDispatcher _dispatcher;
|
private readonly IDispatcher _dispatcher;
|
||||||
@@ -72,4 +72,12 @@ public class UserController : ControllerBase
|
|||||||
var test = new TestMessage("ovo je test poruka");
|
var test = new TestMessage("ovo je test poruka");
|
||||||
await publisher.PublishAsync(test);
|
await publisher.PublishAsync(test);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("me")]
|
||||||
|
public async Task<ActionResult<GetMeQueryDto>> GetMe(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _dispatcher.Execute(new GetMeQuery(), cancellationToken);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,18 @@ builder.Services.AddOpenApi();
|
|||||||
|
|
||||||
builder.Services.AddAips(builder.Configuration);
|
builder.Services.AddAips(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("frontend", policy =>
|
||||||
|
{
|
||||||
|
policy
|
||||||
|
.WithOrigins("http://localhost:5173")
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
await app.Services.InitializeInfrastructureAsync();
|
await app.Services.InitializeInfrastructureAsync();
|
||||||
@@ -24,6 +36,8 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseCors("frontend");
|
||||||
|
|
||||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|||||||
55
front/src/components/AuthStatus.vue
Normal file
55
front/src/components/AuthStatus.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-status">
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<span class="loading">Loading...</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isAuthenticated">
|
||||||
|
<span class="username">{{ user?.username ?? user?.email }}</span>
|
||||||
|
<button @click="logout" class="logout">Logout</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot>
|
||||||
|
<!-- Default: show nothing or provide links in parent -->
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const store = useAuthStore()
|
||||||
|
const user = computed(() => store.user)
|
||||||
|
const isAuthenticated = computed(() => store.isAuthenticated)
|
||||||
|
const isLoading = computed(() => store.isLoading)
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
store.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// ensure store is hydrated; safe to call multiple times
|
||||||
|
void store.initialize()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.username {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.logout {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,7 +13,7 @@ const pinia = createPinia()
|
|||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
auth.initialize()
|
await auth.initialize()
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,90 @@
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
// A small fetch-based HTTP client with automatic token attach and refresh-on-401.
|
||||||
|
// This avoids circular imports by reading/writing tokens directly from localStorage.
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
|
const ACCESS_TOKEN = 'auth_token'
|
||||||
|
const REFRESH_TOKEN = 'refresh_token'
|
||||||
|
|
||||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
async function parseJsonOrThrow(res: Response) {
|
||||||
const auth = useAuthStore()
|
const text = await res.text()
|
||||||
|
try {
|
||||||
const headers: Record<string, string> = {
|
return text ? JSON.parse(text) : undefined
|
||||||
'Content-Type': 'application/json',
|
} catch (e) {
|
||||||
|
// non-JSON response
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (auth.token) {
|
async function refreshTokens(): Promise<{ accessToken?: string; refreshToken?: string } | null> {
|
||||||
headers['Authorization'] = `Bearer ${auth.token}`
|
const refreshToken = localStorage.getItem(REFRESH_TOKEN)
|
||||||
}
|
if (!refreshToken) return null
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}${path}`, {
|
const res = await fetch('/api/User/refresh-login', {
|
||||||
method,
|
method: 'POST',
|
||||||
headers,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: JSON.stringify({ refreshToken }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) return null
|
||||||
const text = await res.text()
|
const data = await parseJsonOrThrow(res)
|
||||||
throw new Error(text || `Request failed: ${res.status}`)
|
const accessToken = data?.accessToken ?? data?.token ?? null
|
||||||
|
const newRefresh = data?.refreshToken ?? null
|
||||||
|
if (accessToken) {
|
||||||
|
localStorage.setItem(ACCESS_TOKEN, accessToken)
|
||||||
|
}
|
||||||
|
if (newRefresh) {
|
||||||
|
localStorage.setItem(REFRESH_TOKEN, newRefresh)
|
||||||
|
}
|
||||||
|
return { accessToken, refreshToken: newRefresh }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T = any>(method: string, url: string, body?: any, attemptRefresh = true): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
const token = localStorage.getItem(ACCESS_TOKEN)
|
||||||
|
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||||
|
|
||||||
|
const payload = body ?? {}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(payload) : null,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 401 && attemptRefresh) {
|
||||||
|
// try refreshing tokens once
|
||||||
|
const refreshed = await refreshTokens()
|
||||||
|
if (refreshed && refreshed.accessToken) {
|
||||||
|
// retry original request with new token
|
||||||
|
const retryHeaders: Record<string, string> = {}
|
||||||
|
if (body !== undefined && body !== null) retryHeaders['Content-Type'] = 'application/json'
|
||||||
|
retryHeaders['Authorization'] = `Bearer ${refreshed.accessToken}`
|
||||||
|
|
||||||
|
const retryRes = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: retryHeaders,
|
||||||
|
body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
if (!retryRes.ok) {
|
||||||
|
const errorBody = await parseJsonOrThrow(retryRes)
|
||||||
|
throw Object.assign(new Error(retryRes.statusText || 'Request failed'), { status: retryRes.status, body: errorBody })
|
||||||
|
}
|
||||||
|
return await parseJsonOrThrow(retryRes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.status === 204) return undefined as T
|
if (!res.ok) {
|
||||||
|
const errorBody = await parseJsonOrThrow(res)
|
||||||
|
throw Object.assign(new Error(res.statusText || 'Request failed'), { status: res.status, body: errorBody })
|
||||||
|
}
|
||||||
|
|
||||||
return res.json() as Promise<T>
|
return await parseJsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
get: <T>(path: string) => request<T>('GET', path),
|
get: <T = any>(url: string) => request<T>('GET', url),
|
||||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
post: <T = any>(url: string, body?: any) => request<T>('POST', url, body),
|
||||||
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
put: <T = any>(url: string, body?: any) => request<T>('PUT', url, body),
|
||||||
delete: <T>(path: string) => request<T>('DELETE', path),
|
delete: <T = any>(url: string, body?: any) => request<T>('DELETE', url, body),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import type { AuthResponse, LoginCredentials, SignupCredentials, User } from '@/types'
|
import type { AuthResponse, LoginCredentials, SignupCredentials, User } from '@/types'
|
||||||
|
import { api } from './api'
|
||||||
|
|
||||||
// TODO: Wire up to real API endpoints via `api` helper
|
function normalizeAuthResponse(raw: any): AuthResponse {
|
||||||
// import { api } from './api'
|
const accessToken = raw?.accessToken ?? raw?.AccessToken ?? raw?.token ?? raw?.Token ?? raw?.access_token ?? null
|
||||||
|
const refreshToken = raw?.refreshToken ?? raw?.RefreshToken ?? raw?.refresh_token ?? null
|
||||||
|
return { accessToken, refreshToken }
|
||||||
|
}
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
async login(_credentials: LoginCredentials): Promise<AuthResponse> {
|
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
||||||
// TODO: return api.post<AuthResponse>('/auth/login', credentials)
|
const raw = await api.post<any>('/api/User/login', credentials)
|
||||||
throw new Error('Not implemented')
|
return normalizeAuthResponse(raw)
|
||||||
},
|
},
|
||||||
|
|
||||||
async signup(_credentials: SignupCredentials): Promise<AuthResponse> {
|
async refreshLogin(refreshToken: string): Promise<AuthResponse> {
|
||||||
// TODO: return api.post<AuthResponse>('/auth/signup', credentials)
|
const raw = await api.post<any>('/api/User/refresh-login', { refreshToken })
|
||||||
throw new Error('Not implemented')
|
return normalizeAuthResponse(raw)
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async signup(credentials: SignupCredentials): Promise<AuthResponse> {
|
||||||
// TODO: return api.post<void>('/auth/logout')
|
const raw = await api.post<any>('/api/User/signup', credentials)
|
||||||
throw new Error('Not implemented')
|
return normalizeAuthResponse(raw)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getCurrentUser(): Promise<User> {
|
async logout(refreshToken: string): Promise<void> {
|
||||||
// TODO: return api.get<User>('/auth/me')
|
return api.delete<void>('/api/User/logout', {refreshToken: refreshToken})
|
||||||
throw new Error('Not implemented')
|
},
|
||||||
|
|
||||||
|
async logoutAll(): Promise<void> {
|
||||||
|
return api.delete<void>('/api/User/logout-all')
|
||||||
|
},
|
||||||
|
|
||||||
|
async getMe() : Promise<User> {
|
||||||
|
const raw = await api.get<any>('/api/User/me')
|
||||||
|
// backend User may have fields like userName / UserName and email / Email
|
||||||
|
const username = raw?.userName ?? raw?.UserName ?? raw?.username ?? raw?.name ?? ''
|
||||||
|
const email = raw?.email ?? raw?.Email ?? ''
|
||||||
|
return { username, email }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,165 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { User, LoginCredentials, SignupCredentials } from '@/types'
|
import type { User, LoginCredentials, SignupCredentials, AuthResponse } from '@/types'
|
||||||
|
import { authService } from '@/services/authService'
|
||||||
|
|
||||||
const TOKEN_KEY = 'auth_token'
|
const ACCESS_TOKEN = 'auth_token'
|
||||||
|
const REFRESH_TOKEN = 'refresh_token'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref<User | null>(null)
|
const user = ref<User | null>(null)
|
||||||
const token = ref<string | null>(null)
|
const accessToken = ref<string | null>(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!user.value)
|
const isAuthenticated = computed(() => !!user.value)
|
||||||
|
|
||||||
function initialize() {
|
// single-flight promise for refresh to avoid concurrent refresh requests
|
||||||
const saved = localStorage.getItem(TOKEN_KEY)
|
let refreshPromise: Promise<void> | null = null
|
||||||
if (saved) {
|
|
||||||
token.value = saved
|
function setTokens(access: string | null, refresh: string | null) {
|
||||||
// TODO: call authService.getCurrentUser() to validate token & hydrate user
|
accessToken.value = access
|
||||||
user.value = { username: 'User', email: 'user@example.com' }
|
if (access) {
|
||||||
|
localStorage.setItem(ACCESS_TOKEN, access)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(ACCESS_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
localStorage.setItem(REFRESH_TOKEN, refresh)
|
||||||
|
} else if (refresh === null) {
|
||||||
|
// explicit null means remove
|
||||||
|
localStorage.removeItem(REFRESH_TOKEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRefresh(): Promise<void> {
|
||||||
|
// if a refresh is already in progress, return that promise
|
||||||
|
if (refreshPromise) return refreshPromise
|
||||||
|
|
||||||
|
const refreshToken = localStorage.getItem(REFRESH_TOKEN)
|
||||||
|
if (!refreshToken) {
|
||||||
|
// nothing to do
|
||||||
|
throw new Error('No refresh token')
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await authService.refreshLogin(refreshToken)
|
||||||
|
// API expected to return { accessToken, refreshToken }
|
||||||
|
setTokens(res.accessToken ?? null, res.refreshToken ?? null)
|
||||||
|
} finally {
|
||||||
|
// clear so subsequent calls can create a new promise
|
||||||
|
refreshPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
const saved = localStorage.getItem(ACCESS_TOKEN)
|
||||||
|
const savedRefresh = localStorage.getItem(REFRESH_TOKEN)
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
accessToken.value = saved
|
||||||
|
try {
|
||||||
|
user.value = await authService.getMe()
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
|
} catch (e) {
|
||||||
|
// token might be expired; try refresh if we have refresh token
|
||||||
|
if (savedRefresh) {
|
||||||
|
try {
|
||||||
|
await tryRefresh()
|
||||||
|
user.value = await authService.getMe()
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
// refresh failed - fallthrough to clearing auth
|
||||||
|
console.warn('Token refresh failed during initialize', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not authenticated or refresh failed
|
||||||
|
setTokens(null, null)
|
||||||
|
user.value = null
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(credentials: LoginCredentials) {
|
async function login(credentials: LoginCredentials) {
|
||||||
// TODO: const res = await authService.login(credentials)
|
isLoading.value = true
|
||||||
// Mock successful response for now
|
error.value = null
|
||||||
const res = {
|
try {
|
||||||
user: { username: credentials.email.split('@')[0] ?? credentials.email, email: credentials.email },
|
const res: AuthResponse = await authService.login(credentials)
|
||||||
token: 'mock-jwt-token',
|
// expect AuthResponse to have accessToken and refreshToken
|
||||||
|
setTokens(res.accessToken ?? null, res.refreshToken ?? null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
user.value = await authService.getMe()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Logged in but failed to fetch user profile', e)
|
||||||
|
// keep tokens but clear user
|
||||||
|
user.value = null
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? 'Login failed'
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
user.value = res.user
|
|
||||||
token.value = res.token
|
|
||||||
localStorage.setItem(TOKEN_KEY, res.token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signup(credentials: SignupCredentials) {
|
async function signup(credentials: SignupCredentials) {
|
||||||
// TODO: const res = await authService.signup(credentials)
|
isLoading.value = true
|
||||||
// Mock successful response for now
|
error.value = null
|
||||||
const res = {
|
|
||||||
user: { username: credentials.username, email: credentials.email },
|
try {
|
||||||
token: 'mock-jwt-token',
|
const res: AuthResponse = await authService.signup(credentials)
|
||||||
|
setTokens(res.accessToken ?? null, res.refreshToken ?? null)
|
||||||
|
try {
|
||||||
|
user.value = await authService.getMe()
|
||||||
|
} catch (e) {
|
||||||
|
// keep tokens but no profile
|
||||||
|
user.value = null
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? 'Signup failed'
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
user.value = res.user
|
|
||||||
token.value = res.token
|
|
||||||
localStorage.setItem(TOKEN_KEY, res.token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
async function logout(allDevices = false) {
|
||||||
user.value = null
|
isLoading.value = true
|
||||||
token.value = null
|
error.value = null
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
try {
|
||||||
|
if (allDevices) await authService.logoutAll()
|
||||||
|
else await authService.logout(localStorage.getItem(REFRESH_TOKEN)!)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore network errors on logout
|
||||||
|
console.warn('Logout request failed', e)
|
||||||
|
} finally {
|
||||||
|
setTokens(null, null)
|
||||||
|
user.value = null
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, token, isAuthenticated, initialize, login, signup, logout }
|
return {
|
||||||
|
user,
|
||||||
|
accessToken,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
initialize,
|
||||||
|
login,
|
||||||
|
signup,
|
||||||
|
logout,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ export interface SignupCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
user: User
|
accessToken: string
|
||||||
token: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,27 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:5266',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
|||||||
Reference in New Issue
Block a user