From 89237d447fea364a680f1be2faa6bdcfb7a174c5 Mon Sep 17 00:00:00 2001 From: Oleh Astappiev Date: Fri, 9 Aug 2024 18:28:17 +0200 Subject: [PATCH] feat: refactor login, use "send email to login" --- interweb-server/pom.xml | 8 ++ .../java/de/l3s/interweb/server/Roles.java | 1 + .../components/auth/PrincipalAugmentor.java | 2 +- .../server/config/ExceptionMappers.java | 33 ++++++ .../server/features/chat/ChatMessage.java | 1 - .../interweb/server/features/user/ApiKey.java | 1 - .../interweb/server/features/user/User.java | 44 ++------ .../server/features/user/UserToken.java | 69 ++++++++++++ .../server/features/user/UsersResource.java | 106 ++++++++++++++++-- .../src/main/resources/application.properties | 10 +- .../db/migration/V1.0.0__Interweb.sql | 21 ++-- ....2.0__Rename_user_token_to_user_apikey.sql | 18 +++ .../V1.3.0__Change_user_login_by_email.sql | 24 ++++ ....0.0__Rename_user_token_to_user_apikey.sql | 4 - 14 files changed, 279 insertions(+), 63 deletions(-) create mode 100644 interweb-server/src/main/java/de/l3s/interweb/server/config/ExceptionMappers.java create mode 100644 interweb-server/src/main/java/de/l3s/interweb/server/features/user/UserToken.java create mode 100644 interweb-server/src/main/resources/db/migration/V1.2.0__Rename_user_token_to_user_apikey.sql create mode 100644 interweb-server/src/main/resources/db/migration/V1.3.0__Change_user_login_by_email.sql delete mode 100644 interweb-server/src/main/resources/db/migration/V2.0.0__Rename_user_token_to_user_apikey.sql 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`;