Leo/Backend #82

Merged
Maxime merged 11 commits from Leo/Backend into master 2024-03-11 19:16:20 +01:00
24 changed files with 1008 additions and 314 deletions
Showing only changes of commit d8a6d06b7a - Show all commits

View File

@ -0,0 +1,36 @@
# Authentification
## Contexte
Le projet demande de pouvoir authentifier les utilisateurs présents. Le but étant de leurs associer un "contexte"
(cours, informations personnelles, ...). Pour que ceux-ci puissent accomplir différentes actions nécéssitantes une
identification (permission, info personelles, ...).
## Méthode
Lorsque qu'un utilisateur se connecte au serveur, nous lui envoyons un token qui sera stocké dans le
navigateur. Ce token est unique à l'utilisateur et pourra être ré-envoyé dans les futures requêtes
pour identifier l'utilisateur.
Ce token est donc une chaine de 64 caractères suivant la norme ISO_8859_1(8bits par cararctère) aléatoires,ce qui est d'après nos recherches suffisant.
De plus une limite de 5 token par utilisateur sera ajoutée de sorte à
1) S'assurer qu'une personne ne noie la base de donnée de tokens.
2) Ajouter une protection supplémentaire pour assurer qu'un token est bien unique à un utilisateur.
## Autres méthodes envisagée
### Oauth2
C'est un protocol d'identification vastement utilisé permettant, en plus d'identifier les requettes,
de gérer leurs permissions. Un utilisateur créen un token peut lui attribuer des permissions
spécifique qui restrainderaients les permissions d'utilisation de ce token. C'est très utile pour
déployer des api de site pouvant notament être accédé par des ordinateurs / bots. Ca n'est en
revanche pas l'objectif du projet et l'option n'a donc pas été retenue
### Spring Sessions / Tomcat sessions
Il aurait été possible de laisser une librairie automatiser les sessions. Malheuresement, celà
implique de devoir se plier au format de la dite librairie. L'implémentation d'un système de gestion
de token maison semblai à la fois, non-imposible et interessant à notre apprentisage. C'est pourquoi
nous n'avons pas utilisé cette option.

View File

@ -1,4 +1,5 @@
package ovh.herisson.Clyde.EndPoints;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -11,13 +12,26 @@ import java.util.Date;
@CrossOrigin(origins = "http://localhost:5173")
public class LoginController {
private final AuthenticatorService authServ;
public LoginController(AuthenticatorService authServ){
this.authServ = authServ;
}
@PostMapping("/login")
public ResponseEntity<String> login(@RequestParam String identifier, String password, Date expirationDate){
String sessionToken = authServ.login(identifier,password,expirationDate);
static public class RequestLogin{
private final String identifier;
private final String password;
@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss")
private final Date expirationDate;
public RequestLogin(String identifier, String password, Date expirationDate){
this.identifier = identifier;
this.password = password;
this.expirationDate = expirationDate;
}
}
public LoginController(AuthenticatorService authServ){
this.authServ = authServ;
}
@PostMapping(value = "/login")
public ResponseEntity<String> login(@RequestBody RequestLogin requestLogin){
String sessionToken = authServ.login(requestLogin.identifier,requestLogin.password,requestLogin.expirationDate);
if (sessionToken == null){
return new UnauthorizedResponse<>("Identifier or Password incorrect");
}

View File

@ -6,7 +6,7 @@ import ovh.herisson.Clyde.Repositories.TokenRepository;
import ovh.herisson.Clyde.Tables.Token;
import ovh.herisson.Clyde.Tables.User;
import java.nio.charset.StandardCharsets;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Calendar;
@ -25,9 +25,15 @@ public class TokenService {
public String generateNewToken(){
byte[] bytes = new byte[64];
new SecureRandom().nextBytes(bytes);
String token = new String(bytes, StandardCharsets.US_ASCII);
System.out.println(token);
return token;
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (((bytes[i]+256)%256 %95+ 32));
}
// will never end up in the catch because of the way that SecureRandom.nextBytes is implemented
try {
return new String(bytes,"ISO_8859_1");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public User getUserFromToken(String token){
@ -57,4 +63,4 @@ public class TokenService {
}
}
};
}
}

View File

@ -19,9 +19,3 @@ npm run dev
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clyde - Login</title>
</head>
<body>
<div id="app"></div>
<script type=module src="/src/login.js"></script>
</body>
</html>

View File

@ -8,6 +8,7 @@
"name": "clyde",
"version": "0.0.0",
"dependencies": {
"vite-plugin-top-level-await": "^1.4.1",
"vue": "^3.4.15",
"vue3-toastify": "^0.2.1"
},
@ -35,7 +36,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
@ -51,7 +51,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -67,7 +66,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -83,7 +81,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -99,7 +96,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -115,7 +111,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -131,7 +126,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -147,7 +141,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -163,7 +156,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -179,7 +171,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -195,7 +186,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -211,7 +201,6 @@
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -227,7 +216,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -243,7 +231,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -259,7 +246,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -275,7 +261,6 @@
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -291,7 +276,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -307,7 +291,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@ -323,7 +306,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@ -339,7 +321,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
@ -355,7 +336,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -371,7 +351,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -387,7 +366,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -401,6 +379,22 @@
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@rollup/plugin-virtual": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
"integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
@ -408,7 +402,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -421,7 +414,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -434,7 +426,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -447,7 +438,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -460,7 +450,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -473,7 +462,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -486,7 +474,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -499,7 +486,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -512,7 +498,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -525,7 +510,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -538,7 +522,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -551,7 +534,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -564,17 +546,212 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.5.tgz",
"integrity": "sha512-4/JGkG4b1Z/QwCGgx+Ub46MlzrsZvBk5JSkxm9PcZ4bSX81c+4Y94Xm3iLp5Ka8NxzS5rD4mJSpcYuN3Tw0ceg==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.2",
"@swc/types": "^0.1.5"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.4.5",
"@swc/core-darwin-x64": "1.4.5",
"@swc/core-linux-arm-gnueabihf": "1.4.5",
"@swc/core-linux-arm64-gnu": "1.4.5",
"@swc/core-linux-arm64-musl": "1.4.5",
"@swc/core-linux-x64-gnu": "1.4.5",
"@swc/core-linux-x64-musl": "1.4.5",
"@swc/core-win32-arm64-msvc": "1.4.5",
"@swc/core-win32-ia32-msvc": "1.4.5",
"@swc/core-win32-x64-msvc": "1.4.5"
},
"peerDependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.5.tgz",
"integrity": "sha512-toMSkbByHNfGXESyY1aiq5L3KutgijrNWB/THgdHIA1aIbwtrgMdFQfxpSE+INuuvWYi/Fxarv86EnU7ewbI0Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.5.tgz",
"integrity": "sha512-LN8cbnmb4Gav8UcbBc+L/DEthmzCWZz22rQr6fIEHMN+f0d71fuKnV0ca0hoKbpZn33dlzUmXQE53HRjlRUQbw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.5.tgz",
"integrity": "sha512-suRFkhBWmOQxlM4frpos1uqjmHfaEI8FuJ0LL5+yRE7IunNDeQJBKujGZt6taeuxo1KqC0N0Ajr8IluN2wrKpA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.5.tgz",
"integrity": "sha512-mLKxasQArDGmR6k9c0tkPVUdoo8VfUecocMG1Mx9NYvpidJNaZ3xq9nYM77v7uq1fQqrs/59DM1fJTNRWvv/UQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.5.tgz",
"integrity": "sha512-pgKuyRP7S29U/HMDTx+x8dFcklWxwB9cHFNCNWSE6bS4vHR93jc4quwPX9OEQX5CVHxm+c8+xof043I4OGkAXw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.5.tgz",
"integrity": "sha512-srR+YN86Oerzoghd0DPCzTbTp08feeJPSr9kkNdmtQWENOa4l/9cJV3+XY6vviw0sEjezPmYnc3SwRxJRaxvEw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.5.tgz",
"integrity": "sha512-aSf41LZtDeG5VXI4RCnzcu0UInPyNm3ip8Kw+sCK+sSqW9o7DgBkyqqbip3RZq84fNUHBQQQQdKXetltsyRRqw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.5.tgz",
"integrity": "sha512-vU3k8JwRUlTkJMfJQY9E4VvLrsIFOpfhnvbuXB84Amo1cJsz+bYQcC6RSvY7qpaDzDKFdUGbJco4uZTRoRf7Mg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.5.tgz",
"integrity": "sha512-856YRh3frRK2XbrSjDOFBgoAqWJLNRkaEtfGzXfeEoyJlOz0BFsSJHxKlHAFkxRfHe2li9DJRUQFTEhXn4OUWw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.5.tgz",
"integrity": "sha512-j1+kV7jmWY1+NbXAvxAEW165781yLXVZKLcoXIZKmw18EatqMF6w8acg1gDG8C+Iw5aWLkRZVS4pijSh7+DtCQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/types": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw=="
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.0.4",
@ -796,7 +973,6 @@
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@ -848,7 +1024,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -1090,7 +1265,6 @@
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz",
"integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
},
@ -1202,11 +1376,22 @@
"requires-port": "^1.0.0"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vite": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
"postcss": "^8.4.35",
@ -1257,6 +1442,19 @@
}
}
},
"node_modules/vite-plugin-top-level-await": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.1.tgz",
"integrity": "sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==",
"dependencies": {
"@rollup/plugin-virtual": "^3.0.2",
"@swc/core": "^1.3.100",
"uuid": "^9.0.1"
},
"peerDependencies": {
"vite": ">=2.8"
}
},
"node_modules/vue": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"vite-plugin-top-level-await": "^1.4.1",
"vue": "^3.4.15",
"vue3-toastify": "^0.2.1"
},

View File

@ -1,8 +1,30 @@
# English translations (some examples to remove)
login.guest.login=log in
login.guest.register=register
login.guest.welcome=Please Register here
login.success=You are now registered as $name
login.guest.signin=Sign in
login.guest.register=Register
login.guest.alregister=Already Registered
login.guest.welcome=WELCOME TO THE UNIVERSITY
login.guest.email=E-MAIL
login.guest.firstname= FIRSTNAME
login.guest.surname=SURNAME
login.guest.country=COUNTRY
login.guest.address=ADDRESS
login.guest.password=PASSWORD
login.guest.nextpage=Next Page
login.guest.lastpage=Last Page
login.guest.submit=Submit
login.guest.birthday=BIRTHDAY
login.guest.confirm=CONFIRM
app.home=Home
app.login=Login
app.notifications=Notifications
app.settings=Settings
app.messages=Messages
app.forum=Forum
app.schedules=Schedules
app.inscription.requests=Inscription Requests
request.moreInfos=More Infos
request.accept=Accept
request.refuse=Refuse
#=====================================================

View File

@ -1,8 +1,29 @@
# Traductions françaises (Quelques examples a enlever)
login.guest.login=s'identifier
login.guest.register=s'enregistrer
login.guest.welcome=Veuillez vous enregistrer ici
login.success=Vous êtes maintenant identifié comme $name
login.guest.signin=SE CONNECTER
login.guest.register=S'enregistrer
login.guest.alregister=Déjà Enregistré
login.guest.welcome=BIENVENUE A L'UNIVERSITE
login.guest.email=E-MAIL
login.guest.firstname= PRENOM
login.guest.surname= NOM
login.guest.country= PAYS
login.guest.address=ADRESSE
login.guest.password= MOT DE PASSE
login.guest.nextpage=Prochaine Page
login.guest.lastpage=Derniere Page
login.guest.submit=Envoyer
login.guest.birthday=DATE DE NAISSANCE
login.guest.confirm=CONFIRMER
app.home=Home
app.login=Se connecter
app.notifications=Notifications
app.settings=Options
app.messages=Messages
app.forum=Forum
app.schedules=Horaires
app.inscription.requests=Demandes d'Inscription
request.moreInfos=Plus d'Infos
request.accept=Accepter
request.refuse=Refuser
#=====================================================

View File

@ -1,15 +1,34 @@
<script setup>
import 'https://kit.fontawesome.com/fb3bbd0a95.js'
import { toast } from 'vue3-toastify';
import { ref } from 'vue'
import { toast } from 'vue3-toastify';
import { ref, computed } from 'vue'
import i18n, { setLang } from './i18n.js'
// Liste des apps
import LoginPage from './Apps/Login.vue'
import Inscription from "./Apps/Inscription.vue"
const apps = {
'/login': LoginPage,
'/inscription': Inscription
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
currentPath.value = window.location.hash
})
const currentView = computed(() => {
return apps[currentPath.value.slice(1) || '/']
})
const home=ref(i18n("app.home"))
const notifications=ref(i18n("app.notifications"))
const settings=ref(i18n("app.settings"))
const login=ref(i18n("app.login"))
const test = ref();
async function draw(e) {
test.value = (await fetch("http://localhost:8080/ping"));
test.value = await test.value.json();
toast(test.value['txt']);
}
</script>
<template>
@ -17,26 +36,30 @@
<div class="topBar">
<ul class="horizontal">
<li title="Home">
<li title=home>
<a href="#home">
<img @click="draw" src="./assets/Clyde.png" style="width: 40px; height: auto; margin-top:4px">
</a></li>
<li title="Home">
<li title=home>
<a href="#home">
<div class=" fa-solid fa-house" style="margin-top: 7px; margin-bottom: 3px;"></div>
</a></li>
<li style="float: right;" title="Account">
<a href="/login">
<li style="float: right;" title=login>
<a href="#/login">
<div class="fa-solid fa-user" style="margin-top: 7px; margin-bottom: 3px;"></div>
</a></li>
<li style="float: right;" title="Notifications">
<li style="float: right;" title=notifications>
<a href="#Notifications">
<div class="fa-solid fa-bell" style="margin-top: 7px; margin-bottom: 3px;"></div>
</a></li>
<li style="float: right;" title="Options">
<li style="float: right;" title=settings>
<a href="#Options">
<div class="fa-solid fa-gear" style="margin-top: 7px; margin-bottom: 3px;"></div>
</a></li>
<li style="float:right; margin-top:7.5px;" title="Language">
<input type="checkbox" @:click="setLang( toggle? 'fr' : 'en' )" v-model="toggle" class="theme-checkbox">
</li>
</ul>
</div>
@ -45,26 +68,31 @@
<li style="margin-top: 25px;" >
<a href="#Messages">
<div class="fa-solid fa-comment" style="font-size: 40px;"></div>
<div class="text">Messages</div>
<div class="text">{{i18n("app.messages")}}</div>
</a></li>
<li >
<a href="#Notifications">
<div class="fa-solid fa-bell" style="font-size: 40px;" ></div>
<div class="text">Notifications</div>
<div class="text">{{i18n("app.notifications")}}</div>
</a></li>
<li >
<a href="#Schedule">
<div class="fa-solid fa-calendar-days" style="font-size: 40px;"></div>
<div class="text">Schedules</div>
<div class="text">{{i18n("app.schedules")}}</div>
</a></li>
<li ><a href="#Forum">
<div class="fa-solid fa-envelope" style="font-size: 40px;" ></div>
<div class="text">Forum</div></a></li>
<div class="text">{{i18n("app.forum")}}</div></a></li>
<li><a href="#/inscription">
<div class="fa-solid fa-users" style="font-size: 40px;"></div>
<div class="text">{{i18n("app.inscription.requests")}}</div></a></li>
</ul>
</div>
<div class="page">
<div style="background-color: rgb(239,50,168); margin:50px;">Il FAUDRA INSERER LA PAGE ICI</div>
<div style=" margin:50px;">
<component :is="currentView" />
</div>
</div>
</div>
</template>
@ -193,4 +221,51 @@
transition-duration: .3s;
padding-left: 5px;
}
.theme-checkbox {
--toggle-size: 16px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 80px;
height: 40px;
background: -webkit-gradient(linear, left top, right top, color-stop(50%, #efefef), color-stop(50%, #2a2a2a)) no-repeat;
background: -o-linear-gradient(left, #efefef 50%, rgb(239, 60, 168) 50%) no-repeat;
background: linear-gradient(to right, #efefef 50%, rgb(239, 60, 168) 50%) no-repeat;
background-size: 205%;
background-position: 0;
-webkit-transition: 0.4s;
-o-transition: 0.4s;
transition: 0.4s;
border-radius: 99em;
position: relative;
cursor: pointer;
font-size: var(--toggle-size);
}
.theme-checkbox::before {
content: "";
width: 35px;
height: 35px;
position: absolute;
top: 2px;
left: 3px;
background: -webkit-gradient(linear, left top, right top, color-stop(50%, #efefef), color-stop(50%, #2rgb(239, 60, 168))) no-repeat;
background: -o-linear-gradient(left, #efefef 50%, rgb(239, 60, 168) 50%) no-repeat;
background: linear-gradient(to right, #efefef 50%, rgb(239, 60, 168) 50%) no-repeat;
background-size: 205%;
background-position: 100%;
border-radius: 50%;
-webkit-transition: 0.4s;
-o-transition: 0.4s;
transition: 0.4s;
}
.theme-checkbox:checked::before {
left: calc(100% - 35px - 3px);
background-position: 0;
}
.theme-checkbox:checked {
background-position: 100%;
}
</style>

View File

@ -0,0 +1,32 @@
<script setup>
import Req from "./Request.vue"
const requests_example = [ {
id:0,
type:"Inscription",
lastName:"DoefenschmirtzLEMAGNIFIQUE",
firstName:"Jhon",
address: "Radiator Springs",
country: "USA",
birthdate:"2004-02-02",
email:"JohnDoe@gmail.com",
cursus:"IT",
degree:"BAC1",
},
{
id:1,
type:"ReInscription",
lastName:"Doe",
firstName:"Jane",
address: "Radiator Springs",
country: "USA",
birthdate:"2004-03-03",
email:"JaneDoe@gmail.com",
cursus:"Psychology",
degree:"BAC1",
}]
</script>
<template>
<Req v-for="item of requests_example" v-bind="item">
</Req>
</template>

237
frontend/src/Apps/Login.vue Normal file
View File

@ -0,0 +1,237 @@
<script setup>
import { login , register} from '@/rest/Users.js'
import { ref } from 'vue'
import i18n from '@/i18n.js'
const loginPage= ref(true)
const page = ref(0)
const emailID=ref("")
const passwordIN=ref("")
const submitValue= ref(i18n("login.guest.submit"))
const surname=ref("")
const firstname=ref("")
const passwordOUT=ref("")
const passwordConfirm=ref("")
const birthday=ref("")
const emailOUT=ref("")
const address=ref("")
const country=ref("")
const cursus=ref("")
const loginInfos = [{_emailID:emailID},{_passwordIN:passwordIN}]
const registerInfos= [{_surname:surname},{_firstname:firstname},{_birthday:birthday},{_passwordOUT:passwordOUT},
{_passwordConfirm:passwordConfirm},{_emailOUT:emailOUT},{_address:address},{_country:country},{_cursus:cursus}]
</script>
<template>
<div class="logBoxCenterer">
<div class='loginBox'>
<div v-if="loginPage">
<form @submit.prevent="login(emailID, passwordIN)"class="form">
<h1 style="color:rgb(239,60,168); font-family: sans-serif;">
{{i18n("login.guest.signin")}}
</h1>
<div class="inputBox">
<p>ID / {{i18n("login.guest.email")}}</p>
<input type="text" v-model="emailID">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.password")}}</p>
<input type="password" v-model="passwordIN">
</div>
<div class="register">
<a @click="loginPage=!loginPage">{{i18n("login.guest.register")}}</a>
</div>
<div class="inputBox">
<input type="submit" v-model="submitValue">
</div>
</form>
</div>
<div v-else>
<form @submit.prevent="register(surname,firstname,emailOUT)" class="form">
<h1 style="color:rgb(239,60,168); font-family: sans-serif; text-align:center;">
{{i18n("login.guest.welcome")}}
</h1>
<div v-if="page === 0">
<div class="inputBox">
<p>{{i18n("login.guest.surname")}}</p>
<input type="text" v-model="surname">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.firstname")}}</p>
<input type="text" v-model="firstname">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.birthday")}}</p>
<input type="date" v-model="birthday">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.password")}}</p>
<input type="password" v-model="passwordOUT">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.confirm")}} {{i18n("login.guest.password")}}</p>
<input type="password" v-model="passwordConfirm">
</div>
<div class="switchpage">
<button @click="page++">{{i18n("login.guest.nextpage")}}</button>
</div>
<div @click="(loginPage=!loginPage) && (page=0)" class="register">
<a>{{i18n("login.guest.alregister")}}</a>
</div>
</div>
<div v-else>
<div class="inputBox">
<p>{{i18n("login.guest.email")}}</p>
<input type="mail" v-model="emailOUT">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.address")}}</p>
<input type="text" v-model="address">
</div>
<div class="inputBox">
<p>{{i18n("login.guest.country")}}</p>
<input type="text" v-model="country">
</div>
<div class="inputBox">
<p>CURSUS</p>
<select v-model="cursus">
<option value="Chemistry">Chemistry</option>
<option value="Psycho">Psychology</option>
<option value="IT">IT</option>
</select>
</div>
<div style="align-self:center;" class="inputBox">
<button style="margin-top:25px;" @click="console.log(outputs)">{{i18n("login.guest.submit")}}</button>
</div>
<div class="switchpage">
<button @click="page--">{{i18n("login.guest.lastpage")}}</button>
</div>
<div @click="(loginPage=!loginPage) && (page=0)" class="register">
<a>{{i18n("login.guest.alregister")}}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped>
.Home{
position:absolute;
display: flex;
z-index: 100;
padding: 8px 16px;
color:rgb(255, 255, 255);
text-decoration: none;
}
.Home:hover{
width:40px;
background-color: black;
border-radius:6px;
color:white;
transform: translate(0px ,1px);
}
.logBoxCenterer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.loginBox {
background-color: rgb(24,24,24);
width: 400px;
display:flex;
justify-content: center;
padding: 40px;
border-radius: 20px;
box-shadow:0 5px 25px #000000;
}
.form {
position:relative;
width:100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items:center;
gap: 15px;
}
.inputBox input,button,select {
width:100%;
background:rgb(255, 0 255);
border: none;
margin-right: 50px;
padding-left: 10px;
padding-top:10px;
padding-bottom:10px;
outline:none;
border-radius: 4px;
font-size:1.35em;
}
.inputBox p{
position:relative;
z-index: 100;
font-family:sans-serif ;
color:rgb(239,60,168);
transition:0.5;
}
.register{
color:rgb(239,60,168);
width: 100%;
display:flex;
cursor: pointer;
}
.switchpage{
width:100px;
background:rgb(255, 0 255);
border: none;
padding-right:0;
padding-top:10px;
padding-bottom:10px;
outline:none;
border-radius: 4px;
font-size:0.8em;
align-self:right;
}
input[type=submit],button,select{
margin-bottom:20px;
background-color: rgb(239,60,168);
cursor: pointer;
padding:10px;
font-size:1.35em;
border:none;
border-radius:20px;
}
button:active ,.switchpage:active{
opacity:0.8;
}
</style>

View File

@ -0,0 +1,109 @@
<script setup>
import i18n from "@/i18n.js"
const props = defineProps({
id: Number,
type: String,
lastName: String,
firstName: String,
address: String,
country: String,
birthDate: String,
cursus:String,
degree:String,});
</script>
<template>
<div class="bodu">
<div class="container">
<div class="id"><a>{{id}}</a></div>
<div class="type"><a>{{type}}</a></div>
<div class="surname"><a>{{lastName}}</a></div>
<div class="firstname"><a>{{firstName}}</a></div>
<div class="infos"><button style="background-color:rgb(105,05,105);" >{{i18n("request.moreInfos")}} </button></div>
<div class="accept"><button style="background-color:rgb(0,105,50);">{{i18n("request.accept")}}</button></div>
<div class="refuse"><button style="background-color:rgb(105,0,0);">{{i18n("request.refuse")}}</button></div>
</div>
</div>
</template>
<style scoped>
.container{
color:white;
height:100px;
font-size:20px;
display:grid;
grid-template-columns:[firstCol-start]100px[firstCol-end secondCol-start]150px[secondCol-end thirdCol-start]200px[thirdCol-end fourthCol-start]150px[fourthCol-end]150px[fifthCol-end]150px[sixthCol-end]150px[endCol];
grid-template-areas:
"id type surname firstname infos accept refuse";
column-gap:10px;
}
.infos {
grid-area:infos;
align-self:center;
}
.accept{
grid-area:accept;
align-self:center;
}
.refuse{
grid-area:refuse;
align-self:center;
}
.titles {
grid-area:titles;
background-color:rgb(215,215,215);
}
.id{
grid-area:id;
margin-left:40px;
align-self:center;
}
.type{
grid-area:type;
align-self:center;
}
.surname{
grid-area:surname;
align-self:center;
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis;
}
.firstname{
grid-area:firstname;
align-self:center;
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis;
}
button{
font-size:15px;
height:50px;
width:100px;
border:none;
border-radius:20px;
}
.bodu {
width:100%;
margin-bottom:10px;
border:2px solid black;
border-radius:9px;
background-color:rgb(50,50,50);
}
</style>

View File

@ -1,105 +0,0 @@
<template>
<body>
<div class="logBoxCenterer">
<div class='loginBox'>
<div class="form">
<h1 style="color:rgb(239,60,168); font-family: sans-serif;">SIGN IN</h1>
<div class="inputBox">
<p>USERNAME</p>
<input type="text" required>
</div>
<div class="inputBox">
<p>PASSWORD</p>
<input type="password" required>
</div>
<div class="register">
<a>Register</a>
</div>
<div class="inputBox">
<input type="submit" value="Login">
</div>
</div>
</div>
</div>
</body>
</template>
<style scoped>
.logBoxCenterer {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.loginBox {
background-color: rgb(24,24,24);
position : absolute;
width: 400px;
display:flex;
justify-content: center;
padding: 40px;
border-radius: 20px;
box-shadow:0 5px 25px #000000;
}
.form {
position:relative;
width:100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items:center;
gap: 15px;
}
.inputBox input {
width:100%;
background:rgb(255, 0 255);
border: none;
margin-right: 50px;
padding-left: 10px;
padding-top:10px;
padding-bottom:10px;
outline:none;
border-radius: 4px;
font-size:1.35em;
}
.inputBox p{
position:relative;
z-index: 100;
font-family:sans-serif ;
color:rgb(239,60,168);
transition:0.5;
}
.register{
color:rgb(239,60,168);
width: 100%;
align-items:center;
display:flex;
justify-content: center;
cursor: pointer;
}
input[type = "submit"] {
background-color: rgb(239,60,168);
cursor: pointer;
padding:10px;
font-size:1.35em;
}
input[type = "submit"]:active{
opacity:0.8;
}
</style>

View File

@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -9,7 +9,7 @@
*
*/
import { getCookie } from './utils.js';
import { getCookie, setCookie } from './utils.js';
const default_lang = "EN";
let langs;
@ -34,10 +34,6 @@ export default function i18n(key, options) {
return ret;
}
//
// Those functions are utility functions use by previous exported functions.
//
/**
* Function that load the file with translation from the specified lang and return a dictionnary
* @param select the language to load. could be null to fetch the cookies for an answer
@ -61,3 +57,8 @@ export async function loadLangs(lang){
langs = filteredLines;
}
await loadLangs();
export async function setLang(lang){
setCookie("lang", lang);
await loadLangs();
}

View File

@ -1,7 +0,0 @@
import './assets/main.css'
import 'vue3-toastify/dist/index.css';
import { createApp } from 'vue'
import App from './Login.vue'
createApp(App).mount('#app')

View File

@ -1,5 +1,6 @@
import './assets/main.css'
import 'vue3-toastify/dist/index.css';
import 'https://kit.fontawesome.com/fb3bbd0a95.js'
import { createApp } from 'vue'
import App from './App.vue'

View File

@ -0,0 +1,35 @@
/**
* functions to handle register requests.
*
* TODO: On time of writing, the backend doesn't support these endpoints so it could be modified in the future.
*/
import { restGet } from './restConsumer.js'
/**
* create a new register requests that can be recovered by the registering service
* TODO: add info in the Object (I don't know what will be needed)
*/
export async function createRegister(){
return restPost("/requests/register"});
}
/**
* list all register request in a list of Objects
*/
export async function getRegisters(){
return restGet("/requests/register")
}
/**
* Get info on a particular registering request
*/
export async function getRegisters(id){
return restGet("/requests/register/" + id);
}
/**
* Change the state of a requests.
*/
export async function validateRegister(id, state){
return restPost("/requests/register/" + id, {state: state});
}

View File

@ -0,0 +1,27 @@
import { restGet, restPost } from './restConsumer.js'
export async function login(user, pass, exp){
return restPost("/login", {login: user, password: pass, expiration: exp});
}
export async function register(user, pass, mail){
return restPost("/user", {name: user, password: pass, mail: mail});
}
/**
* get informations on a specific user.
* Leaving the id empty will return the user's value based on his token
* if the user is not authenticated. then an empty array should be returned
*/
export async function getUser(id){
const endpoint = "/user" + id != null ? "/" + id : "";
return restGet(endpoint);
}
/**
* Reserved for secretary roles. Allow to list all user on the plateform
*/
export async function getAllUsers(){
return restGet("/users");
}

View File

@ -0,0 +1,52 @@
/**
* Courses API
*/
import { restGet, restPost, restDelete, restPatch } from './restConsumer.js'
/**
* Create a new course
*/
export async function createCourse(name, credits, faculty, teacher, assistants){
return restPost("/courses", {name: name, credits: credits, faculty: faculty, teacher: teacher, assistants: assistants} )
}
/**
* Delete the specified course
*/
export async function deleteCourse(id){
return restDelete("/course/" + id);
}
/**
* Get informations on a particular course
*
* @param id identification of the course
*
* @return all atribute of the specified course
* - name
* - credits
* - faculty
* - teacher
* - assistants : list
*/
export async function getCourse(id){
return restGet("/course/" + id);
}
/**
* Change the options of a course
*
* @param id the id of the course
* @param changes Object with value to changes
*
* The changes object can contain:
* - name
* - credits
* - faculty
* - teacher
* - assistants: should be a list and will replace all assistants
*/
export async function alterCourse(id, changes){
return restPatch("/course/" + id, changes);
}

View File

@ -0,0 +1,41 @@
/**
* cursus API
*/
import { restGet, restPostn, restDelete, restPatch } from './restConsumer.js'
/**
* Create a new cursus (bundle of courses)
* @param courses list of courses
*/
export async function createCursus(courses){
return restPost("/cursus", {courses: courses} );
}
/**
* Delete the specified cursus
*/
export async function deleteCursus(id){
return restDelete("/cursus/" + id);
}
/**
* Get informations on a particular cursus
*
* @param id identification of the cursus
*
* @return list of courses
*/
export async function getCursus(id){
return restGet("/cursus/" + id);
}
/**
* Modify the courses of a cursus
*
* @param id the id of the cursus
* @param courses list of new courses
*/
export async function alterCursus(id, courses){
return restPatch("/cursus/" + id, courses);
}

View File

@ -2,7 +2,7 @@
* Return the content of a cookie with specified key
* @param key cookie name
*/
function getCookie(key){
export function getCookie(key){
key = key + "="
let cookies = decodeURIComponent(document.cookie).split(";");
for (let el of cookies) {
@ -14,4 +14,12 @@ function getCookie(key){
return "";
}
export {getCookie};
/**
* Return the content of a cookie with specified key
* @param key cookie name
*/
export function setCookie(key, value){
let cookie = key + "=" + value + "; SameSite=Lax";
document.cookie = cookie;
// Here we can apreciate the stupidity of Javascript :/
}

View File

@ -2,21 +2,14 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import topLevelAwait from 'vite-plugin-top-level-await'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
topLevelAwait(),
],
build: {
rollupOptions:{
input:{
main: './index.html',
login: './login/index.html'
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))