Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ jobs:
-Dcn1.binaries="${CN1_BINARIES}" \
-Pcompile-android \
-DbackendAnalysis \
-pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,swing-compat,javafx-compat,compose-compat,fxml-compiler,desktop-compat-runtime -am verify
-pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,swing-compat,javafx-compat,compose-compat,fxml-compiler,desktop-compat-runtime,cn1-app-kit/backend,cn1-app-kit/common -am verify
- name: Type-check the SQLCipher-backed database package
if: ${{ matrix.java-version == 8 }}
env:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ jobs:
for artifact in codenameone-core codenameone-maven-plugin codenameone-platform-feature-catalog \
codenameone-build-hint-catalog codenameone-build-hint-tools \
codenameone-cil-translator codenameone-unity-compat \
cn1app-archetype cn1lib-archetype; do
cn1app-archetype cn1lib-archetype \
cn1-app-kit-common cn1-app-kit-backend cn1-app-kit-lib; do
url="${R2_BASE_URL}/com/codenameone/${artifact}/${GITHUB_REF_NAME}/${artifact}-${GITHUB_REF_NAME}.pom?cb=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
if [ "$code" != "200" ]; then
Expand Down
26 changes: 26 additions & 0 deletions docs/developer-guide/Backend-Beans.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,32 @@ The library itself is never wired at its own build; only the server that uses
it generates the routes and beans. When the server defines a class with the
same name as one in a library, the server's class is used.

[[app-kit]]
==== The app kit

The app kit is a cn1lib built this way, published with Codename One under the
same version. It holds the plumbing a client and server application usually
needs, so a project needs no copy of its own:

* On the client, `com.codename1.appkit.Net` adapts a generated client call to a
pair of success and failure callbacks, `Skeleton` is a loading placeholder,
and `Chart` draws a small bar chart with its `Meter`.
* On the server, `com.codename1.appkit.server.Caller` reports the signed-in
user and their roles, `Text` validates and trims request fields, and
`Grants` ends a user's other sessions. `AccountSecurity` builds the password
encoder, the user store and the registered client a sign-in flow needs.
None of these is a bean of its own: the kit wires nothing into a server
until the server declares it, so a server that takes only `Text` needs no
authorization server.
* The `push` package sends notifications through `PushSender`. It ships with a
sender for the push service, one that prints to the console in development,
one that records what was sent for tests, and one that fails with a clear
message when push has no configuration.

A project adds `cn1-app-kit-lib` to its client dependencies and
`cn1-app-kit-backend` to its server, both at the Codename One version it builds
with.

=== What the build refuses

Everything a container would discover at start-up is checked while the module
Expand Down
24 changes: 24 additions & 0 deletions maven/cn1-app-kit/backend/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.codenameone</groupId>
<artifactId>cn1-app-kit</artifactId>
<version>8.0-SNAPSHOT</version>
</parent>
<artifactId>cn1-app-kit-backend</artifactId>
<packaging>jar</packaging>
<name>Codename One App Kit: server</name>
<!-- Not annotation processed here: the server that uses it is, and the
marker resource META-INF/codenameone/backend-library says so. -->
<dependencies>
<dependency>
<groupId>com.codenameone</groupId>
<artifactId>codenameone-backend</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.appkit.server;

import com.codename1.backend.ResponseStatusException;
import com.codename1.backend.security.Authentication;
import com.codename1.backend.security.GrantedAuthority;
import com.codename1.backend.security.SecurityContextHolder;

import java.util.Iterator;

/// Who is making the request being served.
///
/// The endpoints implement interfaces generated from the shared contract, whose
/// methods take what the app sends and nothing else, so the caller is read from
/// the security context of the request and not from a parameter.
public final class Caller {
private Caller() {
}

/// The signed-in user's name, which is the e-mail address of the account.
/// Answers 401 when nobody is signed in; the filter chain has normally done
/// so already, and this is what stands behind it.
public static String name() {
Authentication who = SecurityContextHolder.getContext().getAuthentication();
if (who == null || !who.isAuthenticated() || who.getName() == null) {
throw new ResponseStatusException(401, "Sign in first");
}
return who.getName();
}

/// Whether the caller holds `role`, named without its `ROLE_` prefix.
public static boolean hasRole(String role) {
Authentication who = SecurityContextHolder.getContext().getAuthentication();
if (who == null || who.getAuthorities() == null) {
return false;
}
String wanted = "ROLE_" + role;
Iterator<? extends GrantedAuthority> it = who.getAuthorities().iterator();
while (it.hasNext()) {
if (wanted.equals(it.next().getAuthority())) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.appkit.server;

import com.codename1.backend.DataSource;
import com.codename1.backend.security.Authentication;
import com.codename1.backend.security.SecurityContextHolder;
import com.codename1.backend.security.oauth2.server.authorization.OAuth2AuthorizationService;
import com.codename1.backend.security.oauth2.server.resource.JwtAuthenticationToken;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/// The sign-ins an account has open, and ending them.
///
/// Signing in leaves a grant in the authorization server's own table, and a
/// refresh token is good for as long as its grant is there: refreshing asks
/// for no password. So a password changed because a phone was lost changes
/// nothing for whoever holds that phone, unless the grants go too.
///
/// An access token already issued is not recalled by this. It is a signed
/// token that is checked without the table, and it runs out on its own within
/// its few minutes; what ends here is the means of getting another.
///
/// Not a component: a library's components are wired into every server that
/// depends on it, and only a server running the authorization server has the
/// [OAuth2AuthorizationService] this needs. Such a server declares it from a
/// `@Bean` method.
public class Grants {
private final DataSource db;
private final OAuth2AuthorizationService authorizations;

public Grants(DataSource db, OAuth2AuthorizationService authorizations) {
this.db = db;
this.authorizations = authorizations;
}

/// Ends every sign-in of an account but the one this request was made
/// with, so the device a password was changed on stays signed in and every
/// other has to give the new password.
public void endOthers(String username) throws IOException {
end(username, current());
}

/// Ends every sign-in of an account.
public void endAll(String username) throws IOException {
end(username, "");
}

/// Ends one sign-in, by the grant id [#current()] answered when it was
/// made. Its refresh token goes with it.
public void endGrant(String id) throws IOException {
if (id != null && id.length() > 0) {
authorizations.remove(id);
}
}

private void end(String username, String kept) throws IOException {
// The table is the authorization server's, which offers a grant by its
// id and no list of an account's: this reads the ids and leaves the
// removing, which takes the grant's tokens with it, to the service.
List rows = db.query("SELECT id FROM cn1_oauth2_authorization WHERE principal_name = ?",
new Object[] {username});
for (int iter = 0; rows != null && iter < rows.size(); iter++) {
Object row = rows.get(iter);
Object id = row instanceof Map ? ((Map) row).get("id") : null;
if (id != null && !kept.equals(String.valueOf(id))) {
authorizations.remove(String.valueOf(id));
}
}
}

/// The id of the grant the request's token was issued under, or empty for
/// a request that carried no such token. The server writes the grant's id
/// before the first dot of the token's own.
public static String current() {
Authentication who = SecurityContextHolder.getContext().getAuthentication();
if (!(who instanceof JwtAuthenticationToken)) {
return "";
}
String id = ((JwtAuthenticationToken) who).getToken().getId();
int dot = id == null ? -1 : id.indexOf('.');
return dot > 0 ? id.substring(0, dot) : "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.appkit.server;

import com.codename1.backend.ResponseStatusException;

/// What the app sends, made safe to store: trimmed, bounded, and one of the
/// words a field allows.
public final class Text {
private Text() {
}

/// `value` trimmed, or empty for null; answers 400 when longer than `max`.
public static String optional(String value, int max, String what) {
String text = value == null ? "" : value.trim();
if (text.length() > max) {
throw new ResponseStatusException(400, "That is too long for " + what);
}
return text;
}

/// `value` trimmed; answers 400 when it is empty or longer than `max`.
public static String required(String value, int max, String what) {
String text = optional(value, max, what);
if (text.length() == 0) {
throw new ResponseStatusException(400, "Tell us " + what);
}
return text;
}

/// `value` when it is one of `allowed`, and `fallback` when it is empty;
/// answers 400 for anything else. The words are compared as written: they
/// are the app's own constants, not something a person types.
public static String oneOf(String value, String[] allowed, String fallback, String what) {
String text = value == null ? "" : value.trim();
if (text.length() == 0) {
return fallback;
}
for (int iter = 0; iter < allowed.length; iter++) {
if (allowed[iter].equals(text)) {
return text;
}
}
throw new ResponseStatusException(400, "That is not a value for " + what);
}

/// The first letters of the first and last words of a name, in upper case
/// when they are ASCII letters: "Dana Doe" is "DD".
public static String initials(String name) {
String text = name == null ? "" : name.trim();
if (text.length() == 0) {
return "";
}
int last = text.lastIndexOf(' ');
StringBuilder out = new StringBuilder(2);
out.append(upper(text.charAt(0)));
if (last > 0 && last + 1 < text.length()) {
out.append(upper(text.charAt(last + 1)));
}
return out.toString();
}

private static char upper(char c) {
// By hand: toUpperCase() follows the server's locale.
return c >= 'a' && c <= 'z' ? (char) (c - ('a' - 'A')) : c;
}
}
Loading
Loading