Merge pull request #1 from emailerfacu-spec/dev

Login, Register y Logout Terminado
This commit is contained in:
emailerfacu-spec
2025-11-14 22:24:55 -03:00
committed by GitHub
16 changed files with 436 additions and 67 deletions
+22 -10
View File
@@ -1,32 +1,44 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '../components/ui/card';
import * as Field from '$lib/components/ui/field/index.js'; import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import type { ComponentProps } from 'svelte'; import type { RegisterDto } from '../../types';
import { register } from '@/hooks/register';
let {showAlert = $bindable() } = $props();
const setAlert = () => showAlert = true;
let dto: RegisterDto = $state({password: "", username: "", email:"", displayName: ""});
let { ...restProps }: ComponentProps<typeof Card.Root> = $props();
</script> </script>
<Card.Root {...restProps}> <Card.Root>
<Card.Header> <Card.Header>
<Card.Title>Registrarse</Card.Title> <Card.Title>Registrarse</Card.Title>
<hr /> <hr />
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<form> <form onsubmit={(e)=>register(e, dto, setAlert)}>
<Field.Group> <Field.Group>
<Field.Field> <Field.Field>
<Field.Label for="name">Nombre Completo</Field.Label> <Field.Label for="name">Nombre de Usuario</Field.Label>
<Input id="name" type="text" placeholder="Juan Pepe" required /> <Input id="name" bind:value={dto.username} type="text" placeholder="JPepe" required />
</Field.Field> </Field.Field>
<Field.Field>
<Field.Label for="name">Nombre Visible</Field.Label>
<Input type="text" bind:value={dto.displayName} placeholder="Juan Pepe" required />
</Field.Field>
<Field.Field> <Field.Field>
<Field.Label for="email">Email</Field.Label> <Field.Label for="email">Email</Field.Label>
<Input id="email" type="email" placeholder="m@ejemplo.com" required /> <Input id="email" type="email" bind:value={dto.email} placeholder="m@ejemplo.com" required />
</Field.Field> </Field.Field>
<Field.Field> <Field.Field>
<Field.Label for="password">Contraseña</Field.Label> <Field.Label for="password">Contraseña</Field.Label>
<Input id="password" type="password" required /> <Input id="password" type="password" bind:value={dto.password} required />
<Field.Description>Debe de tener por lo menos 8 caracteres.</Field.Description> <Field.Description>Debe de tener por lo menos 8 caracteres.</Field.Description>
</Field.Field> </Field.Field>
<Field.Field> <Field.Field>
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
});
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
</div>
+14
View File
@@ -0,0 +1,14 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};
@@ -3,7 +3,13 @@
import * as Card from '@/components/ui/card'; import * as Card from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { FieldGroup, Field, FieldLabel, FieldDescription } from '@/components/ui/field'; import { FieldGroup, Field, FieldLabel, FieldDescription } from '@/components/ui/field';
const id = $props.id(); import type { LoginDto } from '../../../../types';
import { login } from '@/hooks/login';
let {id, showAlert = $bindable() } = $props();
let dto: LoginDto = $state({password: "", username: ""});
const setAlert = () => showAlert = true;
</script> </script>
<Card.Root class="mx-auto w-full max-w-sm"> <Card.Root class="mx-auto w-full max-w-sm">
@@ -12,11 +18,11 @@
<Card.Description>ingrese su usuario para logearse en la cuenta</Card.Description> <Card.Description>ingrese su usuario para logearse en la cuenta</Card.Description>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<form> <form onsubmit="{(e)=>login(e,dto, setAlert)}">
<FieldGroup> <FieldGroup>
<Field> <Field>
<FieldLabel for="email-{id}">Usuario</FieldLabel> <FieldLabel for="email-{id}">Usuario</FieldLabel>
<Input id="email-{id}" type="email" placeholder="m@example.com" required /> <Input bind:value={dto.username} type="text" placeholder="nombre de usuario" required />
</Field> </Field>
<Field> <Field>
<div class="flex items-center"> <div class="flex items-center">
@@ -25,7 +31,7 @@
Te Olvidaste la contraseña? Te Olvidaste la contraseña?
</a> </a>
</div> </div>
<Input id="password-{id}" type="password" required /> <Input bind:value={dto.password} type="password" required />
</Field> </Field>
<Field> <Field>
<Button type="submit" class="w-full">Login</Button> <Button type="submit" class="w-full">Login</Button>
+73 -27
View File
@@ -4,9 +4,45 @@
import ButtonGroup from '@/components/ui/button-group/button-group.svelte'; import ButtonGroup from '@/components/ui/button-group/button-group.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { sesionStore } from '@/stores/usuario';
import { onMount } from 'svelte';
import { apiBase } from '@/stores/url';
let menuOpen = $state(false); let menuOpen = $state(false);
const toggleMenu = () => (menuOpen = !menuOpen); const toggleMenu = () => (menuOpen = !menuOpen);
let showCerrarSesion = $state(false);
onMount(()=>{
sesionStore.subscribe((value)=>{
showCerrarSesion = !!value?.accessToken;
})
});
async function cerrarSesion(){
try{
const req = await fetch($apiBase+"/api/auth/logout", {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${$sesionStore?.accessToken}`
},
credentials: "include"
});
if(req.ok){
sesionStore.reset();
menuOpen = false;
}
}catch{
console.log("fallo el lougout")
} finally{
sesionStore.reset();
}
}
</script> </script>
<header class="border-b bg-background/95 backdrop-blur"> <header class="border-b bg-background/95 backdrop-blur">
@@ -22,19 +58,24 @@
<!-- Desktop menu --> <!-- Desktop menu -->
<div class="hidden flex-1 items-center justify-end md:flex"> <div class="hidden flex-1 items-center justify-end md:flex">
<ButtonGroup> <ButtonGroup>
<Button {#if showCerrarSesion}
variant={page.url.pathname !== '/login' ? 'outline' : 'secondary'} <Button onclick={cerrarSesion}> Cerrar Sesion
href="/login" </Button>
class="text-foreground/60 transition-colors hover:text-foreground/80" {:else}
>Iniciar Sesion</Button <Button
> variant={page.url.pathname !== '/login' ? 'outline' : 'secondary'}
<Button href="/login"
variant={page.url.pathname !== '/register' ? 'outline' : 'secondary'} class="text-foreground/60 transition-colors hover:text-foreground/80"
href="/register" >Iniciar Sesion
class="text-foreground/60 transition-colors hover:text-foreground/80">Registrarse</Button </Button>
> <Button
</ButtonGroup> variant={page.url.pathname !== '/register' ? 'outline' : 'secondary'}
href="/register"
class="text-foreground/60 transition-colors hover:text-foreground/80">Registrarse
</Button>
{/if}
</ButtonGroup>
</div> </div>
<!-- Mobile menu button --> <!-- Mobile menu button -->
@@ -64,20 +105,25 @@
<!-- Mobile menu --> <!-- Mobile menu -->
{#if menuOpen} {#if menuOpen}
<div class="md:hidden" transition:slide> <div class="md:hidden" transition:slide>
<div class="space-y-1 border-t bg-background/95 px-2 pt-2 pb-3"> <div class="space-y-1 border-t bg-background/95 px-2 pt-2 pb-3">
<Button {#if showCerrarSesion}
variant={page.url.pathname !== '/login' ? 'outline' : 'secondary'} <Button onclick={cerrarSesion}> Cerrar Sesion
href="/login" </Button>
class="mb-2 w-full justify-start text-foreground/60 transition-colors hover:text-foreground/80" {:else}
onclick={() => (menuOpen = false)}>Iniciar Sesion</Button <Button
> variant={page.url.pathname !== '/login' ? 'outline' : 'secondary'}
<Button href="/login"
variant={page.url.pathname !== '/register' ? 'outline' : 'secondary'} class="mb-2 w-full justify-start text-foreground/60 transition-colors hover:text-foreground/80"
href="/register" onclick={() => (menuOpen = false)}>Iniciar Sesion</Button
class="w-full justify-start text-foreground/60 transition-colors hover:text-foreground/80" >
onclick={() => (menuOpen = false)}>Registrarse</Button <Button
> variant={page.url.pathname !== '/register' ? 'outline' : 'secondary'}
</div> href="/register"
class="w-full justify-start text-foreground/60 transition-colors hover:text-foreground/80"
onclick={() => (menuOpen = false)}>Registrarse
</Button>
{/if}
</div>
</div> </div>
{/if} {/if}
</header> </header>
+38
View File
@@ -0,0 +1,38 @@
import { apiBase } from "@/stores/url";
import type { LoginDto } from "../../types";
import { sesionStore } from "@/stores/usuario";
import { goto } from "$app/navigation";
export async function login(e:FormDataEvent,dto: LoginDto, callbackfn:()=>void){
e.preventDefault();
if (dto.password == "" || dto.username == "") return;
try {
const { subscribe } = apiBase;
let baseUrl: string = '';
subscribe((value) => {
baseUrl = value;
})();
const req = await fetch(baseUrl + "/api/auth/login", {
method: "POST",
headers:{
"Content-Type": "application/json"
},
credentials: 'include',
body: JSON.stringify(dto)
});
if (req.ok) {
const token = await req.json();
sesionStore.set(token);
goto("/")
} else {
callbackfn();
}
} catch {
callbackfn();
console.error("fallo al intentar alcanzar el servidor")
}
}
+36
View File
@@ -0,0 +1,36 @@
import { apiBase } from "@/stores/url";
import { goto } from "$app/navigation";
import type { RegisterDto } from "../../types";
export async function register(e:FormDataEvent,dto: RegisterDto, callbackfn:()=>void){
e.preventDefault();
if (dto.password == "" || dto.username == "" ||
!dto.email?.includes("@") || dto.displayName=="") return;
try {
const { subscribe } = apiBase;
let baseUrl: string = '';
subscribe((value) => {
baseUrl = value;
})();
const req = await fetch(baseUrl + "/api/auth/register", {
method: "POST",
headers:{
"Content-Type": "application/json"
},
body: JSON.stringify(dto)
});
if (req.ok) {
const data= await req.json();
goto("/login?msg="+data.message);
} else {
callbackfn();
}
} catch {
callbackfn();
console.error("fallo al intentar alcanzar el servidor")
}
}
+11
View File
@@ -0,0 +1,11 @@
import { writable } from 'svelte/store';
import type { Sesion } from '../../types';
export const currentSesion = writable<Sesion| null>(null);
export const sesionStore = {
subscribe: currentSesion.subscribe,
set: currentSesion.set,
update: currentSesion.update,
reset: () => currentSesion.set(null)
};
+21 -4
View File
@@ -2,12 +2,29 @@
import Card from '@/components/ui/card/card.svelte'; import Card from '@/components/ui/card/card.svelte';
import type { Post } from '../types'; import type { Post } from '../types';
import { Content } from '@/components/ui/card'; import { Content } from '@/components/ui/card';
import { apiBase } from '@/stores/url';
interface Props { $effect(()=>{
posts: Post[]; getPosts();
} });
let posts: Post[] = $state([]);
async function getPosts() {
const { subscribe } = apiBase;
let baseUrl: string = '';
subscribe((value) => {
baseUrl = value;
})();
const req = await fetch(`${baseUrl}/api/posts/timeline?pageSize=3`);
if (req.ok){
posts = await req.json();
}
}
let { posts = [] }: Props = $props();
</script> </script>
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
-15
View File
@@ -1,16 +1 @@
import { apiBase } from '@/stores/url';
export const ssr = true; export const ssr = true;
export async function load({}) {
const { subscribe } = apiBase;
let baseUrl: string = '';
subscribe((value) => {
baseUrl = value;
})();
const req = await fetch(`${baseUrl}/Posts`);
if (req.ok) return { posts: req };
else return { posts: [] };
}
+5
View File
@@ -0,0 +1,5 @@
export function load({ url }) {
return {
message: url.searchParams.get('msg')
};
}
+55 -4
View File
@@ -1,9 +1,60 @@
<script> <script lang="ts">
import LoginForm from '@/components/ui/login-form/login-form.svelte'; import * as Alert from '@/components/ui/alert';
</script> import LoginForm from '@/components/ui/login-form/login-form.svelte';
import AlertCircleIcon from "@lucide/svelte/icons/alert-circle";
import { fade, fly } from 'svelte/transition';
import Info from '@lucide/svelte/icons/info';
let {data} = $props();
let showAlert: boolean = $state(false);
let message = $state(data.message);
$effect(()=>{
resetAlert();
if (data.message) {
history.replaceState(history.state, "", "/login");
setTimeout(() => {
message = "";
}, 7000);
}
});
async function resetAlert (){
if (showAlert == true){
await new Promise(res => setTimeout(res, 2000));
showAlert=false;
}
}
</script>
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
<LoginForm /> <div class="w-full max-w-sm">
{#if message}
<div class="mb-2" transition:fly>
<Alert.Root>
<Info />
<Alert.Title>Info</Alert.Title>
<Alert.Description>
Ingrese las credenciales de la cuenta recien creada
</Alert.Description>
</Alert.Root>
</div>
{/if}
<LoginForm bind:showAlert={showAlert} id="1" />
{#if showAlert}
<div class="mt-2" transition:fade>
<Alert.Root variant="destructive">
<AlertCircleIcon />
<Alert.Title>No se pudo iniciar sesion</Alert.Title>
<Alert.Description>
Revise su usuario o contraseña
</Alert.Description>
</Alert.Root>
</div>
{/if} {/if}
</div> </div>
+32 -3
View File
@@ -1,9 +1,38 @@
<script> <script lang="ts">
import SignupForm from '@/components/signup-form.svelte'; import SignupForm from '@/components/signup-form.svelte';
import AlertCircleIcon from "@lucide/svelte/icons/alert-circle";
import * as Alert from '@/components/ui/alert';
import { fade } from 'svelte/transition';
let showAlert: boolean = $state(false);
$effect(()=>{
resetAlert();
});
async function resetAlert (){
if (showAlert == true){
await new Promise(res => setTimeout(res, 2000));
showAlert=false;
}
}
</script> </script>
<div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10"> <div class="flex min-h-fit w-full items-center justify-center p-6 md:p-10">
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<SignupForm /> <SignupForm bind:showAlert={showAlert} />
{#if showAlert}
<div class="mt-2" transition:fade>
<Alert.Root variant="destructive">
<AlertCircleIcon />
<Alert.Title>No se pudo crear la cuenta</Alert.Title>
<Alert.Description>
Intente nuevamente.
</Alert.Description>
</Alert.Root>
</div>
{/if}
</div> </div>
</div> </div>
+32
View File
@@ -12,3 +12,35 @@ export interface Post {
visibility: string; visibility: string;
hashtags?: string[]; hashtags?: string[];
} }
export interface User {
_id: string;
displayName: string;
username: string;
email: string;
passwordHash: string;
bio?: string;
profileImageUrl?: string;
createdAt: Date;
followersCount: number;
followingCount: number;
refreshTokens: RefreshToken[];
}
export interface Sesion {
accessToken:string?;
message:string;
url:string;
displayname:string;
}
export interface LoginDto {
username: string?;
password: string?;
}
export interface RegisterDto {
username: string?;
email: string?;
password: string?;
displayName: string?;
}