diff --git a/interweb-server/pom.xml b/interweb-server/pom.xml
index bcfef3ca..b7c1a40b 100644
--- a/interweb-server/pom.xml
+++ b/interweb-server/pom.xml
@@ -100,6 +100,10 @@
quarkus-logging-sentry
+
+ io.quarkus
+ quarkus-mailer
+
io.quarkus
quarkus-smallrye-jwt
@@ -121,6 +125,10 @@
org.flywaydb
flyway-mysql
+
+ io.quarkus
+ quarkus-jdbc-mariadb
+
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/Roles.java b/interweb-server/src/main/java/de/l3s/interweb/server/Roles.java
index 464d6ae9..b56f0118 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/Roles.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/Roles.java
@@ -4,6 +4,7 @@ public final class Roles {
private Roles() {
}
+ public static final String ADMIN = "Admin";
public static final String USER = "User"; // Auth by username and password
public static final String APPLICATION = "Application"; // Auth by api key
}
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/components/auth/PrincipalAugmentor.java b/interweb-server/src/main/java/de/l3s/interweb/server/components/auth/PrincipalAugmentor.java
index 5658111f..c9e6c385 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/components/auth/PrincipalAugmentor.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/components/auth/PrincipalAugmentor.java
@@ -23,7 +23,7 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe
}
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
- return User.findByName(identity.getPrincipal().getName()).map(principal -> {
+ return User.findByEmail(identity.getPrincipal().getName()).map(principal -> {
builder.setPrincipal(principal);
return builder.build();
});
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/config/ExceptionMappers.java b/interweb-server/src/main/java/de/l3s/interweb/server/config/ExceptionMappers.java
new file mode 100644
index 00000000..9ffcc5ff
--- /dev/null
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/config/ExceptionMappers.java
@@ -0,0 +1,33 @@
+package de.l3s.interweb.server.config;
+
+import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.core.Response;
+
+import org.jboss.resteasy.reactive.RestResponse;
+import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
+
+public class ExceptionMappers {
+ @ServerExceptionMapper
+ public RestResponse mapException(BadRequestException x) {
+ return RestResponse.status(Response.Status.BAD_REQUEST, HttpError.of(x));
+ }
+
+ public static final class HttpError {
+ private String message;
+
+ public String getMessage() {
+ return message;
+ }
+
+ public static HttpError of(Exception e) {
+ HttpError error = new HttpError();
+ error.message = e.getMessage();
+ return error;
+ }
+
+ @Override
+ public String toString() {
+ return message;
+ }
+ }
+}
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/features/chat/ChatMessage.java b/interweb-server/src/main/java/de/l3s/interweb/server/features/chat/ChatMessage.java
index c4fc7fed..9837aaba 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/features/chat/ChatMessage.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/features/chat/ChatMessage.java
@@ -36,7 +36,6 @@ public class ChatMessage extends PanacheEntityBase {
@NotEmpty
@NotNull
- @Column(columnDefinition = "TEXT")
public String content;
@CreationTimestamp
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/ApiKey.java b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/ApiKey.java
index e14dd73e..f5571bd5 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/ApiKey.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/ApiKey.java
@@ -43,7 +43,6 @@ public class ApiKey extends PanacheEntityBase implements Credential {
@NotEmpty
@NotNull
- @Column(unique = true, length = LENGTH)
public String apikey;
@CreationTimestamp
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/User.java b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/User.java
index 65a32767..c571c53f 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/User.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/User.java
@@ -1,17 +1,18 @@
package de.l3s.interweb.server.features.user;
import java.security.Principal;
-import java.util.Set;
+import java.time.Instant;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
-import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -31,45 +32,24 @@ public class User extends PanacheEntityBase implements Principal {
@NotNull
public String email;
- @NotEmpty
- @NotNull
- @Schema(writeOnly = true)
- @JsonIgnore
- public String password;
-
@NotNull
@Schema(readOnly = true)
public String role = Roles.USER;
- @JsonIgnore
- @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, orphanRemoval = true)
- public Set tokens;
+ @NotNull
+ public boolean approved = false;
- protected User() {
- }
+ @UpdateTimestamp
+ public Instant updated;
- /**
- * Adds a new user to the database
- */
- public static Uni add(String email, String password) {
- User user = new User();
- user.email = email;
- user.password = BcryptUtil.bcryptHash(password);
- return user.persist();
- }
+ @CreationTimestamp
+ public Instant created;
- public static Uni findByName(String name) {
- return find("email", name).firstResult();
+ protected User() {
}
- public static Uni findByNameAndPassword(String name, String password) {
- return findByName(name).onItem().ifNotNull().transform(user -> {
- if (BcryptUtil.matches(password, user.password)) {
- return user;
- }
-
- return null;
- });
+ public static Uni findByEmail(String name) {
+ return find("email", name).firstResult();
}
@Override
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UserToken.java b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UserToken.java
new file mode 100644
index 00000000..faa7a031
--- /dev/null
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UserToken.java
@@ -0,0 +1,69 @@
+package de.l3s.interweb.server.features.user;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
+import io.quarkus.security.credential.Credential;
+import io.smallrye.mutiny.Uni;
+import org.hibernate.annotations.CreationTimestamp;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import de.l3s.interweb.core.util.StringUtils;
+
+@Entity
+@Cacheable
+@Table(name = "user_token")
+public class UserToken extends PanacheEntityBase implements Credential {
+
+ public enum Type {
+ login(Duration.ofHours(6), 32);
+
+ private final Duration duration;
+ private final int size;
+
+ Type(Duration duration, int size) {
+ this.duration = duration;
+ this.size = size;
+ }
+ }
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ public Long id;
+
+ @JsonIgnore
+ @ManyToOne(optional = false, fetch = FetchType.EAGER)
+ public User user;
+
+ @NotEmpty
+ @NotNull
+ public String type;
+
+ @NotEmpty
+ @NotNull
+ public String token;
+
+ @CreationTimestamp
+ public Instant created;
+
+ public UserToken() {
+ // required for Panache
+ }
+
+ public static UserToken generate(Type type) {
+ UserToken key = new UserToken();
+ key.type = type.name();
+ key.token = StringUtils.randomAlphanumeric(type.size);
+ return key;
+ }
+
+ public static Uni findByToken(Type type, String token) {
+ return find("type = ?1 and token = ?2 and created > ?3", type.name(), token, Instant.now().minus(type.duration)).firstResult();
+ }
+}
diff --git a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UsersResource.java b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UsersResource.java
index 9673357c..7f80c132 100644
--- a/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UsersResource.java
+++ b/interweb-server/src/main/java/de/l3s/interweb/server/features/user/UsersResource.java
@@ -1,5 +1,6 @@
package de.l3s.interweb.server.features.user;
+import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
@@ -8,14 +9,19 @@
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.UriInfo;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
+import io.quarkus.mailer.Mail;
+import io.quarkus.mailer.reactive.ReactiveMailer;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.mutiny.Uni;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
+import org.jboss.logging.Logger;
import de.l3s.interweb.server.Roles;
@@ -24,6 +30,39 @@
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UsersResource {
+ private static final Logger log = Logger.getLogger(UsersResource.class);
+
+ private static final String NEW_USER_EMAIL_SUBJECT = "Interweb: New user awaiting approval";
+ private static final String NEW_USER_EMAIL_BODY = """
+ There is a new user awaiting approval:
+ %s
+
+ Best regards,
+ Interweb Team
+ """;
+
+ private static final String LOGIN_APPROVAL_REQUIRED = "Thank you for registration, unfortunately your account is not yet approved. Please wait until we have reviewed your registration.";
+ private static final String LOGIN_EMAIL_SUBJECT = "Interweb: Login Link";
+ private static final String LOGIN_EMAIL_BODY = """
+ Hello,
+
+ To login to Interweb, please click the following link:
+ %s
+
+ This link is valid for 6 hours.
+
+ Best regards,
+ Interweb Team
+ """;
+
+ @ConfigProperty(name = "interweb.admin.email")
+ String adminEmail;
+
+ @ConfigProperty(name = "interweb.auto-approve.pattern")
+ String autoApprovePattern;
+
+ @Inject
+ ReactiveMailer mailer;
@Context
SecurityIdentity securityIdentity;
@@ -32,20 +71,67 @@ public class UsersResource {
@Path("/register")
@WithTransaction
@Operation(summary = "Register a new user", description = "Use this method to register a new user")
- public Uni register(@Valid CreateUser user) {
- return User.findByName(user.email)
- .onItem().ifNotNull().failWith(() -> new BadRequestException("User already exists"))
- .chain(() -> User.add(user.email, user.password));
+ public Uni register(@Valid CreateUser user, @Context UriInfo uriInfo) {
+ return login(user.email, uriInfo);
}
@GET
@Path("/login")
@Produces(MediaType.TEXT_PLAIN)
- @Operation(summary = "Request JWT token for the given email and password", description = "Use this method to login to the app and manage tokens")
- public Uni login(@NotEmpty @QueryParam("email") String email, @NotEmpty @QueryParam("password") String password) {
- return User.findByNameAndPassword(email, password)
- .onItem().ifNotNull().transform(user -> Jwt.upn(user.getName()).groups(Roles.USER).sign())
- .onItem().ifNull().failWith(() -> new BadRequestException("No user found or password is incorrect"));
+ @Operation(summary = "Request JWT token for the given email", description = "Use this method to login to the app and manage tokens")
+ public Uni login(@NotEmpty @QueryParam("email") String email, @Context UriInfo uriInfo) {
+ return findOrCreateUser(email).chain(user -> {
+ if (!user.approved) {
+ return Uni.createFrom().failure(new BadRequestException(LOGIN_APPROVAL_REQUIRED));
+ } else {
+ return createAndSendToken(user, uriInfo);
+ }
+ }).chain(() -> Uni.createFrom().item("The login link has been sent to your email."));
+ }
+
+ private Uni findOrCreateUser(String email) {
+ return User.findByEmail(email).onItem().ifNull().switchTo(() -> createUser(email).call(user -> {
+ if (!user.approved) {
+ return mailer.send(Mail.withText(adminEmail, NEW_USER_EMAIL_SUBJECT, NEW_USER_EMAIL_BODY.formatted(user.email)));
+ }
+
+ return Uni.createFrom().voidItem();
+ }));
+ }
+
+ private Uni createAndSendToken(User user, UriInfo uriInfo) {
+ return createToken(user)
+ .chain(token -> {
+ log.infof("Login token %s created for user %s", token.token, user.email);
+ String tokenUrl = uriInfo.getBaseUri() + "jwt?token=" + token.token;
+ return mailer.send(Mail.withText(user.email, LOGIN_EMAIL_SUBJECT, LOGIN_EMAIL_BODY.formatted(tokenUrl)));
+ });
+ }
+
+ @WithTransaction
+ protected Uni createUser(String email) {
+ User user = new User();
+ user.email = email;
+ user.approved = email.matches(autoApprovePattern);
+ user.role = Roles.USER;
+ return user.persist();
+ }
+
+ @WithTransaction
+ protected Uni createToken(User user) {
+ UserToken token = UserToken.generate(UserToken.Type.login);
+ token.user = user;
+ return token.persist();
+ }
+
+ @GET
+ @Path("/jwt")
+ @Produces(MediaType.TEXT_PLAIN)
+ @Operation(summary = "Request JWT token for the given email and password")
+ public Uni jwt(@NotEmpty @QueryParam("token") String token) {
+ return UserToken.findByToken(UserToken.Type.login, token)
+ .onItem().ifNotNull().transform(loginToken -> Jwt.upn(loginToken.user.getName()).groups(loginToken.user.role).sign())
+ .onItem().ifNull().failWith(() -> new BadRequestException("The token is invalid or expired"));
}
@GET
@@ -56,6 +142,6 @@ public User me() {
return (User) securityIdentity.getPrincipal();
}
- public record CreateUser(@NotNull @NotEmpty @Email @Size(max = 255) String email, @NotNull @NotEmpty @Size(max = 255) String password) {
+ public record CreateUser(@NotNull @NotEmpty @Email @Size(max = 255) String email) {
}
}
diff --git a/interweb-server/src/main/resources/application.properties b/interweb-server/src/main/resources/application.properties
index abc2701c..e63970ac 100644
--- a/interweb-server/src/main/resources/application.properties
+++ b/interweb-server/src/main/resources/application.properties
@@ -1,3 +1,6 @@
+interweb.admin.email=
+interweb.auto-approve.pattern=.+@example.test$
+
quarkus.http.cors=true
quarkus.http.cors.origins=/.*/
quarkus.http.cors.access-control-allow-credentials=true
@@ -18,14 +21,13 @@ quarkus.cache.caffeine."models".expire-after-write=PT10M
quarkus.native.additional-build-args=-march=x86-64-v2
quarkus.datasource.db-kind=mariadb
+quarkus.datasource.jdbc.url=jdbc:${quarkus.datasource.reactive_url}
quarkus.datasource.reactive.max-size=20
-
-quarkus.hibernate-orm.database.generation=update
-quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
%dev.quarkus.hibernate-orm.log.sql=true
quarkus.flyway.active=true
quarkus.flyway.migrate-at-start=true
+quarkus.flyway.baseline-on-migrate = true
quarkus.health.openapi.included=false
quarkus.smallrye-jwt.enabled=true
@@ -36,7 +38,7 @@ jwt.issuer=https://l3s.de/interweb
%dev,test.jwt.key={"kty":"oct","alg":"HS256","k":"EAOrMjRGCUolnkODMzSWGJxjQFY4GxDW-4xFTBu5N1Y"}
smallrye.jwt.new-token.issuer=${jwt.issuer}
-smallrye.jwt.new-token.lifespan=7200
+smallrye.jwt.new-token.lifespan=86400
smallrye.jwt.sign.key=${jwt.key}
mp.jwt.verify.issuer=${jwt.issuer}
diff --git a/interweb-server/src/main/resources/db/migration/V1.0.0__Interweb.sql b/interweb-server/src/main/resources/db/migration/V1.0.0__Interweb.sql
index 16ca7d64..5b815e0b 100644
--- a/interweb-server/src/main/resources/db/migration/V1.0.0__Interweb.sql
+++ b/interweb-server/src/main/resources/db/migration/V1.0.0__Interweb.sql
@@ -1,21 +1,21 @@
CREATE TABLE IF NOT EXISTS `chat`
(
- `id` UUID NOT NULL PRIMARY KEY,
- `token_id` BIGINT UNSIGNED NOT NULL,
- `model` VARCHAR(32) NOT NULL,
- `user` VARCHAR(32) DEFAULT NULL,
- `title` VARCHAR(512) DEFAULT NULL,
- `used_tokens` int(11) NOT NULL DEFAULT 0,
- `estimated_cost` DOUBLE NOT NULL DEFAULT 0,
- `created` DATETIME DEFAULT NOW(),
+ `id` UUID NOT NULL PRIMARY KEY,
+ `token_id` BIGINT UNSIGNED NOT NULL,
+ `model` VARCHAR(32) NOT NULL,
+ `user` VARCHAR(32) DEFAULT NULL,
+ `title` VARCHAR(512) DEFAULT NULL,
+ `used_tokens` int(11) UNSIGNED NOT NULL DEFAULT 0,
+ `estimated_cost` DOUBLE UNSIGNED NOT NULL DEFAULT 0,
+ `created` DATETIME DEFAULT NOW(),
KEY `index_chat_user` (`user`),
CONSTRAINT `fk_chat_token` FOREIGN KEY (`token_id`) REFERENCES `user_token` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS `chat_message`
(
- `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`chat_id` UUID NOT NULL,
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`role` TINYINT UNSIGNED NOT NULL,
`content` TEXT NOT NULL,
`created` DATETIME DEFAULT NOW(),
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS `user`
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
+ `username` VARCHAR(255) NOT NULL,
`role` VARCHAR(255) NOT NULL
);
@@ -39,5 +40,5 @@ CREATE TABLE IF NOT EXISTS `user_token`
`url` VARCHAR(512) DEFAULT NULL,
`description` VARCHAR(1024) DEFAULT NULL,
UNIQUE KEY `index_user_token_apikey` (`apikey`),
- CONSTRAINT `fk_user_token_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
+ CONSTRAINT `fk_user_token_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);
diff --git a/interweb-server/src/main/resources/db/migration/V1.2.0__Rename_user_token_to_user_apikey.sql b/interweb-server/src/main/resources/db/migration/V1.2.0__Rename_user_token_to_user_apikey.sql
new file mode 100644
index 00000000..ca16d9ff
--- /dev/null
+++ b/interweb-server/src/main/resources/db/migration/V1.2.0__Rename_user_token_to_user_apikey.sql
@@ -0,0 +1,18 @@
+ALTER TABLE `user_token`
+ DROP CONSTRAINT `index_user_token_apikey`;
+ALTER TABLE `user_token`
+ DROP CONSTRAINT `fk_user_token_user`;
+
+ALTER TABLE `user_token` RENAME TO `user_apikey`;
+ALTER TABLE `user_apikey`
+ ADD `created` DATETIME DEFAULT NOW() AFTER `apikey`;
+ALTER TABLE `user_apikey`
+ ADD UNIQUE KEY `index_user_apikey_apikey` (`apikey`);
+ALTER TABLE `user_apikey`
+ ADD CONSTRAINT `fk_user_user_apikey` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE `chat`
+ DROP CONSTRAINT `fk_chat_token`;
+ALTER TABLE `chat` RENAME COLUMN `token_id` TO `apikey_id`;
+ALTER TABLE `chat`
+ ADD CONSTRAINT `fk_chat_apikey` FOREIGN KEY (`apikey_id`) REFERENCES `user_apikey` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/interweb-server/src/main/resources/db/migration/V1.3.0__Change_user_login_by_email.sql b/interweb-server/src/main/resources/db/migration/V1.3.0__Change_user_login_by_email.sql
new file mode 100644
index 00000000..83ba716b
--- /dev/null
+++ b/interweb-server/src/main/resources/db/migration/V1.3.0__Change_user_login_by_email.sql
@@ -0,0 +1,24 @@
+ALTER TABLE `user`
+ DROP COLUMN `username`;
+ALTER TABLE `user`
+ DROP COLUMN `password`;
+
+ALTER TABLE `user`
+ ADD COLUMN `approved` BOOL DEFAULT 0 AFTER `role`;
+ALTER TABLE `user`
+ ADD `updated` DATETIME DEFAULT NOW() AFTER `approved`;
+ALTER TABLE `user`
+ ADD `created` DATETIME DEFAULT NOW() AFTER `updated`;
+ALTER TABLE `user`
+ CHANGE COLUMN `role` `role` ENUM ('User', 'Admin') NOT NULL DEFAULT 'User' AFTER `email`;
+
+CREATE TABLE IF NOT EXISTS `user_token`
+(
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ `user_id` BIGINT UNSIGNED NOT NULL,
+ `type` VARCHAR(64) NOT NULL,
+ `token` VARCHAR(255) NOT NULL,
+ `created` DATETIME DEFAULT NOW(),
+ UNIQUE KEY `index_user_token_token` (`token`),
+ CONSTRAINT `fk_user_token_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+);
diff --git a/interweb-server/src/main/resources/db/migration/V2.0.0__Rename_user_token_to_user_apikey.sql b/interweb-server/src/main/resources/db/migration/V2.0.0__Rename_user_token_to_user_apikey.sql
deleted file mode 100644
index db7bfe01..00000000
--- a/interweb-server/src/main/resources/db/migration/V2.0.0__Rename_user_token_to_user_apikey.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE `user_token` RENAME TO `user_apikey`;
-
-ALTER TABLE `user_apikey` ADD `created` DATETIME DEFAULT NOW() AFTER `apikey`;
-ALTER TABLE `chat` RENAME COLUMN `token_id` TO `apikey_id`;